@tablation/crew 0.63.1 → 0.64.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 +1 -1
  2. package/dist/cli.js +515 -183
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5183,7 +5183,7 @@ function doctorSelftestLines(stored, toolchains) {
5183
5183
  }
5184
5184
 
5185
5185
  // src/cli.ts
5186
- import { resolve as resolve5, dirname as dirname18, join as join31, basename as basename10 } from "path";
5186
+ import { resolve as resolve5, dirname as dirname19, join as join32, basename as basename10 } from "path";
5187
5187
  import { fileURLToPath } from "url";
5188
5188
  import { hostname as hostname2, homedir as homedir8 } from "os";
5189
5189
  import { spawnSync as spawnSync6 } from "child_process";
@@ -5209,6 +5209,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
5209
5209
  "repo",
5210
5210
  "role",
5211
5211
  "route",
5212
+ "recipes",
5212
5213
  "ship-name",
5213
5214
  "tail",
5214
5215
  "ticket",
@@ -5762,15 +5763,52 @@ var State = class _State {
5762
5763
  }
5763
5764
  };
5764
5765
 
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";
5766
+ // src/connect-resume.ts
5767
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
5767
5768
  import { dirname as dirname6, join as join14 } from "path";
5769
+ function lastSessionFile(stateDir) {
5770
+ return join14(stateDir, "connect-session.json");
5771
+ }
5772
+ function readPointers(stateDir) {
5773
+ try {
5774
+ const parsed = JSON.parse(readFileSync13(lastSessionFile(stateDir), "utf8"));
5775
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
5776
+ return Object.fromEntries(Object.entries(parsed).filter((e) => typeof e[1] === "string"));
5777
+ } catch {
5778
+ return {};
5779
+ }
5780
+ }
5781
+ function rememberSession(stateDir, host, workspaceSlug) {
5782
+ try {
5783
+ const file = lastSessionFile(stateDir);
5784
+ mkdirSync7(dirname6(file), { recursive: true });
5785
+ writeFileSync8(file, `${JSON.stringify({ ...readPointers(stateDir), [host]: workspaceSlug }, null, 2)}
5786
+ `, { mode: 384 });
5787
+ } catch {
5788
+ }
5789
+ }
5790
+ async function resumeKeychainSession(o) {
5791
+ const workspace = readPointers(o.stateDir)[o.host];
5792
+ if (!workspace) return void 0;
5793
+ try {
5794
+ const stored = await o.store.get(o.host, workspace);
5795
+ if (!stored?.apiKey) return void 0;
5796
+ if (!await o.verify(stored.apiKey)) return void 0;
5797
+ return { workspace, apiKey: stored.apiKey };
5798
+ } catch {
5799
+ return void 0;
5800
+ }
5801
+ }
5802
+
5803
+ // src/reset.ts
5804
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync14, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
5805
+ import { dirname as dirname7, join as join15 } from "path";
5768
5806
  function clonedCheckoutsFile(stateDir) {
5769
- return join14(stateDir, "cloned-checkouts.json");
5807
+ return join15(stateDir, "cloned-checkouts.json");
5770
5808
  }
5771
5809
  function readClonedCheckouts(stateDir) {
5772
5810
  try {
5773
- const parsed = JSON.parse(readFileSync13(clonedCheckoutsFile(stateDir), "utf8"));
5811
+ const parsed = JSON.parse(readFileSync14(clonedCheckoutsFile(stateDir), "utf8"));
5774
5812
  return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
5775
5813
  } catch {
5776
5814
  return [];
@@ -5780,8 +5818,8 @@ function recordClonedCheckout(stateDir, dir) {
5780
5818
  const known = readClonedCheckouts(stateDir);
5781
5819
  if (known.includes(dir)) return;
5782
5820
  const file = clonedCheckoutsFile(stateDir);
5783
- mkdirSync7(dirname6(file), { recursive: true });
5784
- writeFileSync8(file, `${JSON.stringify([...known, dir], null, 2)}
5821
+ mkdirSync8(dirname7(file), { recursive: true });
5822
+ writeFileSync9(file, `${JSON.stringify([...known, dir], null, 2)}
5785
5823
  `);
5786
5824
  }
5787
5825
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -5836,10 +5874,10 @@ function removeCheckout(dir, dryRun, fx) {
5836
5874
  }
5837
5875
  function removeState(stateDir, dryRun) {
5838
5876
  const out = [];
5839
- const targets = [join14(stateDir, "resolved"), join14(stateDir, "keys"), clonedCheckoutsFile(stateDir), join14(stateDir, "reload-requested"), join14(stateDir, "daemon.started")];
5877
+ const targets = [join15(stateDir, "resolved"), join15(stateDir, "keys"), clonedCheckoutsFile(stateDir), join15(stateDir, "connect-session.json"), join15(stateDir, "reload-requested"), join15(stateDir, "daemon.started")];
5840
5878
  try {
5841
5879
  for (const name of readdirSync4(stateDir)) {
5842
- if (name.startsWith(".") && statSync5(join14(stateDir, name)).isFile()) targets.push(join14(stateDir, name));
5880
+ if (name.startsWith(".") && statSync5(join15(stateDir, name)).isFile()) targets.push(join15(stateDir, name));
5843
5881
  }
5844
5882
  } catch {
5845
5883
  }
@@ -6113,18 +6151,18 @@ import { existsSync as existsSync16, statfsSync } from "fs";
6113
6151
  import { execFileSync as execFileSync2 } from "child_process";
6114
6152
  import {
6115
6153
  existsSync as existsSync15,
6116
- mkdirSync as mkdirSync8,
6154
+ mkdirSync as mkdirSync9,
6117
6155
  openSync as openSync3,
6118
6156
  closeSync as closeSync3,
6119
6157
  writeSync,
6120
6158
  statSync as statSync6,
6121
6159
  unlinkSync as unlinkSync2,
6122
- readFileSync as readFileSync14,
6123
- writeFileSync as writeFileSync9,
6160
+ readFileSync as readFileSync15,
6161
+ writeFileSync as writeFileSync10,
6124
6162
  readdirSync as readdirSync5,
6125
6163
  rmSync as rmSync8
6126
6164
  } from "fs";
6127
- import { dirname as dirname7, basename as basename5, join as join15 } from "path";
6165
+ import { dirname as dirname8, basename as basename5, join as join16 } from "path";
6128
6166
  var GitError = class extends Error {
6129
6167
  };
6130
6168
  function streamText(x) {
@@ -6490,7 +6528,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
6490
6528
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
6491
6529
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
6492
6530
  gitOk(cwd, ["worktree", "remove", "--force", path]);
6493
- const root = dirname7(path);
6531
+ const root = dirname8(path);
6494
6532
  if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
6495
6533
  }
6496
6534
  }
@@ -6508,11 +6546,11 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
6508
6546
  }
6509
6547
  for (const name of names) {
6510
6548
  if (!name.startsWith("crew-release-")) continue;
6511
- const root = join15(tmp, name);
6549
+ const root = join16(tmp, name);
6512
6550
  try {
6513
6551
  const st = statSync6(root);
6514
6552
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
6515
- if (existsSync15(join15(root, "checkout"))) continue;
6553
+ if (existsSync15(join16(root, "checkout"))) continue;
6516
6554
  rmSync8(root, { recursive: true, force: true });
6517
6555
  removed.push(name);
6518
6556
  } catch {
@@ -6561,7 +6599,7 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
6561
6599
  `${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`
6562
6600
  );
6563
6601
  }
6564
- mkdirSync8(dirname7(dir), { recursive: true });
6602
+ mkdirSync9(dirname8(dir), { recursive: true });
6565
6603
  const url = cloneUrlFor(remote, opts);
6566
6604
  try {
6567
6605
  execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -6586,7 +6624,7 @@ function sleepSync(ms2) {
6586
6624
  }
6587
6625
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6588
6626
  const lockPath = checkoutLockPath(dir);
6589
- mkdirSync8(dirname7(lockPath), { recursive: true });
6627
+ mkdirSync9(dirname8(lockPath), { recursive: true });
6590
6628
  const deadline = Date.now() + timeoutMs;
6591
6629
  for (; ; ) {
6592
6630
  try {
@@ -6626,14 +6664,14 @@ function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {
6626
6664
  if (!existsSync15(`${dir}/.git`) && githubRemote) {
6627
6665
  const url = cloneUrlFor(githubRemote, clone);
6628
6666
  const marker = cloneFailureMarker(dir);
6629
- if (existsSync15(marker) && readFileSync14(marker, "utf8").split("\n")[0] === url) {
6630
- return { action: "clone-failed", detail: readFileSync14(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
6667
+ if (existsSync15(marker) && readFileSync15(marker, "utf8").split("\n")[0] === url) {
6668
+ return { action: "clone-failed", detail: readFileSync15(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
6631
6669
  }
6632
6670
  try {
6633
6671
  ensureRepoCheckout(dir, githubRemote, clone);
6634
6672
  } catch (e) {
6635
6673
  if (!(e instanceof GitError)) throw e;
6636
- writeFileSync9(marker, `${url}
6674
+ writeFileSync10(marker, `${url}
6637
6675
  ${e.message}`);
6638
6676
  return { action: "clone-failed", detail: e.message, repeat: false };
6639
6677
  }
@@ -8762,8 +8800,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
8762
8800
  }
8763
8801
 
8764
8802
  // src/poll.ts
8765
- import { writeFileSync as writeFileSync10 } from "fs";
8766
- import { join as join16 } from "path";
8803
+ import { writeFileSync as writeFileSync11 } from "fs";
8804
+ import { join as join17 } from "path";
8767
8805
 
8768
8806
  // src/stalled.ts
8769
8807
  var STALLED_AFTER_CYCLES = 30;
@@ -9054,7 +9092,7 @@ async function decideCycle(o) {
9054
9092
  }
9055
9093
  return decision;
9056
9094
  }
9057
- var digestPath = (stateDir, route, role) => join16(stateDir, `digest-${route}-${role}.md`);
9095
+ var digestPath = (stateDir, route, role) => join17(stateDir, `digest-${route}-${role}.md`);
9058
9096
  function writeDigest(o, d, role, stateDir) {
9059
9097
  const me = o.route.resolved?.seats[role];
9060
9098
  if (!me) return false;
@@ -9118,7 +9156,7 @@ function writeDigest(o, d, role, stateDir) {
9118
9156
  };
9119
9157
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
9120
9158
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
9121
- writeFileSync10(path, text);
9159
+ writeFileSync11(path, text);
9122
9160
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
9123
9161
  data: { role, bytes: Buffer.byteLength(text) }
9124
9162
  });
@@ -9496,12 +9534,12 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
9496
9534
 
9497
9535
  // src/run-worktree.ts
9498
9536
  import { existsSync as existsSync18 } from "fs";
9499
- import { dirname as dirname8, join as join18 } from "path";
9537
+ import { dirname as dirname9, join as join19 } from "path";
9500
9538
 
9501
9539
  // src/ports.ts
9502
9540
  import { execFileSync as execFileSync3 } from "child_process";
9503
9541
  import { existsSync as existsSync17 } from "fs";
9504
- import { join as join17 } from "path";
9542
+ import { join as join18 } from "path";
9505
9543
  var run = (cmd, args) => {
9506
9544
  try {
9507
9545
  return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -9546,7 +9584,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
9546
9584
  }
9547
9585
  function worktreeExists(parent, prefix, n) {
9548
9586
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
9549
- if (existsSync17(join17(parent, name, ".git"))) return true;
9587
+ if (existsSync17(join18(parent, name, ".git"))) return true;
9550
9588
  }
9551
9589
  return false;
9552
9590
  }
@@ -9638,7 +9676,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9638
9676
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
9639
9677
  return { action: "recut", path: existing.path, branch, base: baseRef };
9640
9678
  }
9641
- const path = join18(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
9679
+ const path = join19(dirname9(dir), worktreeForTicket(dir, cfg, t, role));
9642
9680
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
9643
9681
  return { action: "cut-remote", path, branch: located.branch };
9644
9682
  }
@@ -9960,8 +9998,8 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
9960
9998
  }
9961
9999
 
9962
10000
  // src/connect-wizard.ts
9963
- import { delimiter, dirname as dirname9, join as join19 } from "path";
9964
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
10001
+ import { delimiter, dirname as dirname10, join as join20 } from "path";
10002
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12 } from "fs";
9965
10003
  import { stringify } from "yaml";
9966
10004
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
9967
10005
  function shouldRunWizard(opts) {
@@ -9983,7 +10021,7 @@ function defaultIsExecutable(p) {
9983
10021
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
9984
10022
  for (const dir of pathEnv.split(delimiter)) {
9985
10023
  if (!dir) continue;
9986
- const candidate = join19(dir, name);
10024
+ const candidate = join20(dir, name);
9987
10025
  if (isExecutable(candidate)) return candidate;
9988
10026
  }
9989
10027
  return void 0;
@@ -10009,9 +10047,9 @@ async function runWizard(p, env) {
10009
10047
  if (entered && isExec(entered)) agentBin = entered;
10010
10048
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
10011
10049
  }
10012
- const nodeDir = dirname9(env.nodePath);
10050
+ const nodeDir = dirname10(env.nodePath);
10013
10051
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
10014
- const baseDirs = [nodeDir, pnpmPath ? dirname9(pnpmPath) : void 0];
10052
+ const baseDirs = [nodeDir, pnpmPath ? dirname10(pnpmPath) : void 0];
10015
10053
  p.say(
10016
10054
  `
10017
10055
  Host Passengers lets this machine run the agent sessions that other people's
@@ -10022,7 +10060,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
10022
10060
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
10023
10061
  }
10024
10062
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
10025
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname9(dockerPath) : void 0]);
10063
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname10(dockerPath) : void 0]);
10026
10064
  if (extraPath) {
10027
10065
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
10028
10066
  }
@@ -10044,9 +10082,9 @@ routes:
10044
10082
  ${routeBlock}`;
10045
10083
  }
10046
10084
  function writeNewConfig(path, text) {
10047
- mkdirSync9(dirname9(path), { recursive: true });
10085
+ mkdirSync10(dirname10(path), { recursive: true });
10048
10086
  try {
10049
- writeFileSync11(path, text, { mode: 384, flag: "wx" });
10087
+ writeFileSync12(path, text, { mode: 384, flag: "wx" });
10050
10088
  return true;
10051
10089
  } catch (e) {
10052
10090
  if (e.code === "EEXIST") return false;
@@ -10079,7 +10117,7 @@ Wrote ${o.configPath}.
10079
10117
 
10080
10118
  // src/connect-json.ts
10081
10119
  import { accessSync as accessSync3, constants as constants3 } from "fs";
10082
- import { dirname as dirname10 } from "path";
10120
+ import { dirname as dirname11 } from "path";
10083
10121
  var CHOICE_EXIT_CODE = 3;
10084
10122
  function createEmitter(json, write) {
10085
10123
  return {
@@ -10127,7 +10165,7 @@ function flagWizardAnswers(f, env) {
10127
10165
  if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
10128
10166
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
10129
10167
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
10130
- const extraPath = f.extraPath ?? deriveExtraPath([dirname10(env.nodePath), pnpmPath ? dirname10(pnpmPath) : void 0, dockerPath ? dirname10(dockerPath) : void 0]);
10168
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
10131
10169
  return {
10132
10170
  answers: {
10133
10171
  name: f.shipName?.trim() || env.hostname,
@@ -10140,8 +10178,8 @@ function flagWizardAnswers(f, env) {
10140
10178
  }
10141
10179
 
10142
10180
  // src/release-run.ts
10143
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync20, copyFileSync, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, rmSync as rmSync9 } from "fs";
10144
- import { dirname as dirname11, join as join20 } from "path";
10181
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, existsSync as existsSync20, copyFileSync, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, rmSync as rmSync9 } from "fs";
10182
+ import { dirname as dirname12, join as join21 } from "path";
10145
10183
  import { tmpdir as tmpdir3 } from "os";
10146
10184
 
10147
10185
  // src/release.ts
@@ -10829,13 +10867,13 @@ async function bump(o, size) {
10829
10867
  }
10830
10868
  return printed;
10831
10869
  }
10832
- const files = o.repo.release.versionFiles.map((f) => join20(o.cwd, f));
10870
+ const files = o.repo.release.versionFiles.map((f) => join21(o.cwd, f));
10833
10871
  const first = files[0];
10834
10872
  if (!existsSync20(first)) {
10835
10873
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
10836
10874
  return null;
10837
10875
  }
10838
- const current = JSON.parse(readFileSync15(first, "utf8")).version;
10876
+ const current = JSON.parse(readFileSync16(first, "utf8")).version;
10839
10877
  if (!current) {
10840
10878
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
10841
10879
  return null;
@@ -10848,27 +10886,27 @@ async function bump(o, size) {
10848
10886
  return version;
10849
10887
  }
10850
10888
  for (const f of files) {
10851
- const src = readFileSync15(f, "utf8");
10889
+ const src = readFileSync16(f, "utf8");
10852
10890
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
10853
10891
  if (out === src) {
10854
10892
  o.emit.error(`no version field in ${f}`);
10855
10893
  return null;
10856
10894
  }
10857
- writeFileSync12(f, out);
10895
+ writeFileSync13(f, out);
10858
10896
  }
10859
10897
  return version;
10860
10898
  }
10861
10899
  function writeChangelog(o, version, entries) {
10862
10900
  const file = o.repo.release.changelog;
10863
10901
  if (!file || entries.length === 0) return;
10864
- const path = join20(o.cwd, file);
10902
+ const path = join21(o.cwd, file);
10865
10903
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
10866
10904
  if (o.dryRun) {
10867
10905
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
10868
10906
  return;
10869
10907
  }
10870
- const existing = existsSync20(path) ? readFileSync15(path, "utf8") : "# Changelog\n";
10871
- writeFileSync12(path, insertChangelogSection(existing, section));
10908
+ const existing = existsSync20(path) ? readFileSync16(path, "utf8") : "# Changelog\n";
10909
+ writeFileSync13(path, insertChangelogSection(existing, section));
10872
10910
  }
10873
10911
  async function confirm(o, expected) {
10874
10912
  if (!o.repo.hooks.released) return void 0;
@@ -10961,8 +10999,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
10961
10999
  function openReleaseCheckout(o) {
10962
11000
  const { remote, base } = o.repo.branch;
10963
11001
  const branch = `crew/release-${Date.now()}`;
10964
- const root = mkdtempSync2(join20(tmpdir3(), "crew-release-"));
10965
- const dir = join20(root, "checkout");
11002
+ const root = mkdtempSync2(join21(tmpdir3(), "crew-release-"));
11003
+ const dir = join21(root, "checkout");
10966
11004
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
10967
11005
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
10968
11006
  try {
@@ -10972,10 +11010,10 @@ function openReleaseCheckout(o) {
10972
11010
  throw e;
10973
11011
  }
10974
11012
  for (const f of o.repo.worktrees.copy) {
10975
- const src = join20(o.cwd, f);
11013
+ const src = join21(o.cwd, f);
10976
11014
  if (!existsSync20(src)) continue;
10977
- mkdirSync10(dirname11(join20(dir, f)), { recursive: true });
10978
- copyFileSync(src, join20(dir, f));
11015
+ mkdirSync11(dirname12(join21(dir, f)), { recursive: true });
11016
+ copyFileSync(src, join21(dir, f));
10979
11017
  }
10980
11018
  return { dir, branch, close };
10981
11019
  }
@@ -12769,8 +12807,8 @@ function describeCapability(i) {
12769
12807
  }
12770
12808
 
12771
12809
  // src/agents-sync-run.ts
12772
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
12773
- import { dirname as dirname12 } from "path";
12810
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync14 } from "fs";
12811
+ import { dirname as dirname13 } from "path";
12774
12812
  async function runAgentsSync(target, o) {
12775
12813
  const dryRun = o.dryRun ?? false;
12776
12814
  const force = o.force ?? false;
@@ -12803,12 +12841,12 @@ async function runAgentsSync(target, o) {
12803
12841
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
12804
12842
  if (!dryRun) {
12805
12843
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
12806
- const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
12844
+ const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
12807
12845
  raw.agentPersonas = personaResult.agentPersonas;
12808
12846
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
12809
12847
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
12810
- mkdirSync11(dirname12(resolvedPath), { recursive: true });
12811
- writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
12848
+ mkdirSync12(dirname13(resolvedPath), { recursive: true });
12849
+ writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
12812
12850
  `);
12813
12851
  if (target.resolved) {
12814
12852
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -12915,7 +12953,7 @@ import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml"
12915
12953
 
12916
12954
  // src/repos-cmd.ts
12917
12955
  import { homedir as homedir7 } from "os";
12918
- import { basename as basename7, join as join21, resolve as resolve3 } from "path";
12956
+ import { basename as basename7, join as join22, resolve as resolve3 } from "path";
12919
12957
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
12920
12958
  var ReposError = class extends Error {
12921
12959
  };
@@ -13113,7 +13151,7 @@ function planRepoClone(i) {
13113
13151
  }
13114
13152
  const ship = doc.get("ship", true);
13115
13153
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
13116
- const target = i.dir ? expandDir(i.dir, i.base) : join21(expandDir(basePath, i.base), i.route.split("/")[0], name);
13154
+ const target = i.dir ? expandDir(i.dir, i.base) : join22(expandDir(basePath, i.base), i.route.split("/")[0], name);
13117
13155
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
13118
13156
  return { ...add, remote, target };
13119
13157
  }
@@ -13167,17 +13205,62 @@ var DETECTORS = [
13167
13205
  { source: "pyproject.toml", detect: fromPyproject },
13168
13206
  { source: "Makefile", detect: fromMakefile }
13169
13207
  ];
13208
+ var WORKFLOW_RE = /\.ya?ml$/;
13209
+ function detectRecipes(f) {
13210
+ const workflows = (f.list?.(".github/workflows") ?? []).filter((n) => WORKFLOW_RE.test(n)).sort();
13211
+ if (workflows.length === 0) return [];
13212
+ const evidence = workflows.map((n) => `.github/workflows/${n}`);
13213
+ return [
13214
+ {
13215
+ id: "github-actions-release",
13216
+ title: "Releases run in GitHub Actions",
13217
+ description: "CI publishes the release after the crew merges; the crew watches `gh release view` for the live tag.",
13218
+ evidence,
13219
+ // `ltrimstr` because `verify.match: version` compares the bare version, not `v1.2.3`.
13220
+ hooks: { released: `gh release view --json tagName -q '.tagName | ltrimstr("v")'` },
13221
+ release: { mode: "ci_auto", ci: "github", verifyMatch: "version" },
13222
+ writable: true,
13223
+ defaultOn: true
13224
+ },
13225
+ {
13226
+ id: "github-actions-ci-status",
13227
+ title: "Check CI status before hand-off",
13228
+ description: "Ask GitHub whether the latest run on the branch passed before a ticket is handed to QA. The repo spec has no hook for this yet, so it is reported but cannot be written.",
13229
+ evidence,
13230
+ hooks: {},
13231
+ check: `gh run list --branch "$CREW_BRANCH" --limit 1 --json conclusion -q '.[0].conclusion == "success"'`,
13232
+ writable: false,
13233
+ defaultOn: false
13234
+ }
13235
+ ];
13236
+ }
13170
13237
  function suggestHooks(f) {
13238
+ const recipes = detectRecipes(f);
13171
13239
  const found = DETECTORS.flatMap((d) => {
13172
13240
  const hooks = d.detect(f);
13173
13241
  return hooks && Object.keys(hooks).length > 0 ? [{ source: d.source, hooks }] : [];
13174
13242
  });
13175
13243
  if (found.length === 0) {
13176
- return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"] };
13244
+ return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"], recipes };
13177
13245
  }
13178
13246
  const [first, ...others] = found;
13179
13247
  const notes = others.length > 0 ? [`also found ${others.map((o) => o.source).join(", ")}; proposing from ${first.source} only`] : [];
13180
- return { source: first.source, hooks: first.hooks, notes };
13248
+ return { source: first.source, hooks: first.hooks, notes, recipes };
13249
+ }
13250
+ function selectRecipes(s, ids) {
13251
+ const chosen = ids.map((id) => {
13252
+ const r = s.recipes.find((x) => x.id === id);
13253
+ if (!r) throw new ReposError(`no recipe "${id}" here (have: ${s.recipes.map((x) => x.id).join(", ") || "none"})`);
13254
+ if (!r.writable) throw new ReposError(`recipe "${id}" cannot be written: ${r.description}`);
13255
+ return r;
13256
+ });
13257
+ let hooks = { ...s.hooks };
13258
+ let release;
13259
+ for (const r of chosen) {
13260
+ hooks = { ...r.hooks, ...hooks };
13261
+ release ??= r.release;
13262
+ }
13263
+ return { hooks, ...release ? { release } : {} };
13181
13264
  }
13182
13265
  function parseHooksArgs(args, routes) {
13183
13266
  const rest = [...args];
@@ -13187,7 +13270,7 @@ function parseHooksArgs(args, routes) {
13187
13270
  if (routes.length === 1) route = routes[0];
13188
13271
  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(", ")}`);
13189
13272
  }
13190
- if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--write route|repo] [--json]");
13273
+ if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--json]");
13191
13274
  return { route, name: rest[0] };
13192
13275
  }
13193
13276
  function repoDirOf(text, base, route, name) {
@@ -13233,7 +13316,7 @@ function planRouteHooks(text, route, name, hooks) {
13233
13316
  }
13234
13317
  return { text: String(doc), written, kept };
13235
13318
  }
13236
- function starterRepoConfig(hooks) {
13319
+ function starterRepoConfig(hooks, release) {
13237
13320
  const lines = [
13238
13321
  "# Starter .crew.yaml proposed by `crew repos hooks --write repo`.",
13239
13322
  "# Review the hooks, then commit this file; it is the durable home for them",
@@ -13246,7 +13329,12 @@ function starterRepoConfig(hooks) {
13246
13329
  const entries = Object.entries(hooks).filter(([, v]) => v !== void 0);
13247
13330
  if (entries.length === 0) lines.push(' # test: "..."', ' # build: "..."');
13248
13331
  for (const [k, v] of entries) lines.push(` ${k}: ${JSON.stringify(v)}`);
13249
- lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
13332
+ if (release) {
13333
+ lines.push("", "release:", ` mode: ${release.mode}`);
13334
+ if (release.ci) lines.push(` ci: { provider: ${release.ci} }`);
13335
+ if (release.verifyMatch) lines.push(" verify:", ` match: ${release.verifyMatch}`);
13336
+ lines.push("");
13337
+ } else lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
13250
13338
  const text = lines.join("\n");
13251
13339
  parseRepoConfig(text, ".crew.yaml");
13252
13340
  return text;
@@ -13255,8 +13343,180 @@ function existingRepoConfig(f) {
13255
13343
  return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
13256
13344
  }
13257
13345
 
13346
+ // src/repos-hooks-agent.ts
13347
+ import { spawn as spawn4, execFileSync as execFileSync4 } from "child_process";
13348
+ import { realpathSync as realpathSync2 } from "fs";
13349
+ import { isAbsolute as isAbsolute2, relative } from "path";
13350
+ var AgentSuggestError = class extends Error {
13351
+ reason;
13352
+ constructor(reason, message) {
13353
+ super(message);
13354
+ this.reason = reason;
13355
+ }
13356
+ };
13357
+ var HOOK_KEYS = ["test", "build", "version", "bump", "released"];
13358
+ var READ_ONLY_TOOLS = ["Read", "Grep", "Glob"];
13359
+ var WRITING_TOOLS = ["Bash", "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch", "Task", "Agent"];
13360
+ var DEFAULT_TIMEOUT_MS = 18e4;
13361
+ function buildAgentPrompt(sniff) {
13362
+ return [
13363
+ "You are reviewing a git checkout (the current directory) to work out how to test and build it.",
13364
+ "Read README*, CLAUDE.md, AGENTS.md, GEMINI.md, package/build manifests and .github/workflows/* as needed.",
13365
+ "You may only read files. Do not modify anything and do not run commands.",
13366
+ "",
13367
+ `A file sniff already proposed: ${JSON.stringify(sniff.hooks)}.`,
13368
+ "Propose the commands a contributor should run, using the repo's own documented ones over guesses.",
13369
+ "Omit any hook you cannot support from a file you read.",
13370
+ "",
13371
+ "Answer with ONLY one JSON object, no prose and no code fence:",
13372
+ '{"hooks": {"test": "...", "build": "...", "version": "...", "bump": "...", "released": "..."}, "notes": ["..."]}',
13373
+ "`version` prints the bare version, `bump` bumps by $CREW_BUMP (patch|minor|major) and prints the new version,",
13374
+ "`released` prints what is live (a version or sha). Keys you cannot support are left out."
13375
+ ].join("\n");
13376
+ }
13377
+ function agentInvocation(spec, model) {
13378
+ if (spec.harness !== "claude") {
13379
+ throw new AgentSuggestError("unsupported_harness", `--agent supports the claude harness only (toolchain "${spec.name}" is ${spec.harness})`);
13380
+ }
13381
+ return wrapInvocation(spec, {
13382
+ model,
13383
+ args: [
13384
+ "-p",
13385
+ "--allowedTools",
13386
+ ...READ_ONLY_TOOLS,
13387
+ "--disallowedTools",
13388
+ ...WRITING_TOOLS,
13389
+ "--model",
13390
+ model,
13391
+ "--output-format",
13392
+ "stream-json",
13393
+ "--verbose",
13394
+ ...spec.args
13395
+ ]
13396
+ });
13397
+ }
13398
+ function parseAgentStream(out, dir) {
13399
+ let text = "";
13400
+ let isError = false;
13401
+ const files = /* @__PURE__ */ new Set();
13402
+ let base = dir;
13403
+ try {
13404
+ base = realpathSync2(dir);
13405
+ } catch {
13406
+ }
13407
+ for (const line of out.split("\n")) {
13408
+ if (!line.trim()) continue;
13409
+ let ev;
13410
+ try {
13411
+ ev = JSON.parse(line);
13412
+ } catch {
13413
+ continue;
13414
+ }
13415
+ if (ev?.type === "assistant" && Array.isArray(ev.message?.content)) {
13416
+ for (const b of ev.message.content) {
13417
+ const p = b?.type === "tool_use" && b.name === "Read" ? b.input?.file_path : void 0;
13418
+ if (typeof p === "string") files.add(isAbsolute2(p) ? relative(base, p) : p);
13419
+ }
13420
+ } else if (ev?.type === "result") {
13421
+ text = typeof ev.result === "string" ? ev.result : "";
13422
+ isError = ev.is_error === true;
13423
+ }
13424
+ }
13425
+ return { text, isError, filesRead: [...files].sort() };
13426
+ }
13427
+ function parseAgentHooks(text) {
13428
+ const start = text.indexOf("{");
13429
+ const end = text.lastIndexOf("}");
13430
+ if (start < 0 || end <= start) throw new AgentSuggestError("bad_output", "the toolchain did not answer with a JSON object");
13431
+ let obj;
13432
+ try {
13433
+ obj = JSON.parse(text.slice(start, end + 1));
13434
+ } catch {
13435
+ throw new AgentSuggestError("bad_output", "the toolchain's answer was not valid JSON");
13436
+ }
13437
+ const raw = obj && typeof obj === "object" && obj.hooks && typeof obj.hooks === "object" ? obj.hooks : {};
13438
+ const hooks = {};
13439
+ for (const k of HOOK_KEYS) if (typeof raw[k] === "string" && raw[k].trim()) hooks[k] = raw[k].trim();
13440
+ const notes = Array.isArray(obj.notes) ? obj.notes.filter((n) => typeof n === "string") : [];
13441
+ return { hooks, notes };
13442
+ }
13443
+ var SIGNED_OUT = /not logged in|\/login|invalid api key|authentication|unauthori[sz]ed|\b401\b|credit balance/i;
13444
+ function classifyFailure(o) {
13445
+ if (o.spawnError?.code === "ENOENT") return "not_installed";
13446
+ return SIGNED_OUT.test(o.output) ? "not_signed_in" : "failed";
13447
+ }
13448
+ function porcelain(dir) {
13449
+ try {
13450
+ return execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
13451
+ } catch {
13452
+ return void 0;
13453
+ }
13454
+ }
13455
+ async function runAgentSuggest(o) {
13456
+ const model = o.model ?? o.spec.model;
13457
+ const { bin, args } = agentInvocation(o.spec, model);
13458
+ const adapter = adapterFor(o.spec.harness);
13459
+ const env = { ...process.env, ...adapter.runEnv(o.spec) };
13460
+ for (const v of adapter.unsetEnv) delete env[v];
13461
+ delete env.CREW_API_KEY;
13462
+ const before = porcelain(o.dir);
13463
+ const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
13464
+ const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
13465
+ const run2 = await new Promise((resolve6) => {
13466
+ let out = "";
13467
+ let err = "";
13468
+ const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
13469
+ let settled = false;
13470
+ const settle = (r) => {
13471
+ if (settled) return;
13472
+ settled = true;
13473
+ signal.removeEventListener("abort", onAbort);
13474
+ resolve6(r);
13475
+ };
13476
+ const onAbort = () => {
13477
+ try {
13478
+ if (child.pid) process.kill(-child.pid, "SIGTERM");
13479
+ } catch {
13480
+ child.kill("SIGTERM");
13481
+ }
13482
+ child.stdout.destroy();
13483
+ child.stderr.destroy();
13484
+ settle({ code: null, out, err, aborted: true });
13485
+ };
13486
+ if (signal.aborted) onAbort();
13487
+ else signal.addEventListener("abort", onAbort, { once: true });
13488
+ child.stdout.on("data", (d) => {
13489
+ out += d.toString();
13490
+ });
13491
+ child.stderr.on("data", (d) => {
13492
+ err += d.toString();
13493
+ });
13494
+ child.on("error", (e) => settle({ code: null, out, err, spawnError: e, aborted: false }));
13495
+ child.on("close", (code) => settle({ code, out, err, aborted: false }));
13496
+ child.stdin.on("error", () => {
13497
+ });
13498
+ child.stdin.end(buildAgentPrompt(o.sniff));
13499
+ });
13500
+ if (run2.aborted || signal.aborted) {
13501
+ throw new AgentSuggestError(o.signal?.aborted ? "cancelled" : "timeout", o.signal?.aborted ? "cancelled" : "the toolchain did not answer in time");
13502
+ }
13503
+ const after = porcelain(o.dir);
13504
+ if (before !== void 0 && after !== before) {
13505
+ throw new AgentSuggestError("modified_checkout", "the checkout changed during --agent; inspect it with `git status` (the agent result was discarded)");
13506
+ }
13507
+ const parsed = parseAgentStream(run2.out, o.dir);
13508
+ if (run2.spawnError || run2.code !== 0 || parsed.isError) {
13509
+ const reason = classifyFailure({ code: run2.code, ...run2.spawnError ? { spawnError: run2.spawnError } : {}, output: `${parsed.text}
13510
+ ${run2.err}` });
13511
+ throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run2.err).trim().slice(0, 300) || `exited ${run2.code}`);
13512
+ }
13513
+ const { hooks, notes } = parseAgentHooks(parsed.text);
13514
+ const differsFromSniff = HOOK_KEYS.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
13515
+ return { source: "agent", hooks, filesRead: parsed.filesRead, differsFromSniff, notes, toolchain: o.spec.name, model };
13516
+ }
13517
+
13258
13518
  // src/sample-ticket.ts
13259
- import { basename as basename8, dirname as dirname14, join as join22 } from "path";
13519
+ import { basename as basename8, dirname as dirname15, join as join23 } from "path";
13260
13520
  var SAMPLE_REPO = "crew-sample";
13261
13521
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
13262
13522
  var SAMPLE_MARKER = "sample: true";
@@ -13279,8 +13539,8 @@ has something safe to work on: its test and build hooks always pass, it has no
13279
13539
  remote, and \`crew sample-ticket --remove\` deletes it.
13280
13540
  `;
13281
13541
  function sampleDir(existing, reposBase, workspace) {
13282
- if (existing.length > 0) return join22(dirname14(existing[0]), SAMPLE_REPO);
13283
- return join22(reposBase, workspace, SAMPLE_REPO);
13542
+ if (existing.length > 0) return join23(dirname15(existing[0]), SAMPLE_REPO);
13543
+ return join23(reposBase, workspace, SAMPLE_REPO);
13284
13544
  }
13285
13545
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
13286
13546
  function sampleDescription() {
@@ -13361,8 +13621,8 @@ function schedulerInstalled(plan, exists, crontab) {
13361
13621
  }
13362
13622
 
13363
13623
  // src/worktree-sweep.ts
13364
- import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
13365
- import { basename as basename9, dirname as dirname15, join as join23, resolve as resolve4 } from "path";
13624
+ import { existsSync as existsSync23, readFileSync as readFileSync18, readdirSync as readdirSync6 } from "fs";
13625
+ import { basename as basename9, dirname as dirname16, join as join24, resolve as resolve4 } from "path";
13366
13626
  function planWorktreeSweep(target, tickets, contract) {
13367
13627
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
13368
13628
  const verified = contract.statuses.verified;
@@ -13416,7 +13676,7 @@ function worktreeTicketNumbers(target) {
13416
13676
  return [...nums];
13417
13677
  }
13418
13678
  function siblingWorktreeDirs(target) {
13419
- const parent = dirname15(target.dir.replace(/[/\\]+$/, ""));
13679
+ const parent = dirname16(target.dir.replace(/[/\\]+$/, ""));
13420
13680
  const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
13421
13681
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
13422
13682
  let names;
@@ -13429,7 +13689,7 @@ function siblingWorktreeDirs(target) {
13429
13689
  for (const name of names) {
13430
13690
  if (!name.startsWith(stem)) continue;
13431
13691
  const m = DIR_NUMBER_SUFFIXED.exec(name);
13432
- const path = join23(parent, name);
13692
+ const path = join24(parent, name);
13433
13693
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
13434
13694
  }
13435
13695
  return out;
@@ -13532,7 +13792,7 @@ function isHandBackMerge(path, base) {
13532
13792
  if (!msgPath) return false;
13533
13793
  let first;
13534
13794
  try {
13535
- first = readFileSync17(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13795
+ first = readFileSync18(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13536
13796
  } catch {
13537
13797
  return false;
13538
13798
  }
@@ -13638,13 +13898,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
13638
13898
 
13639
13899
  // src/stream-sweep.ts
13640
13900
  import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync8 } from "fs";
13641
- import { join as join24 } from "path";
13901
+ import { join as join25 } from "path";
13642
13902
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
13643
13903
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
13644
- const dir = join24(stateDir, "streams");
13904
+ const dir = join25(stateDir, "streams");
13645
13905
  if (!existsSync24(dir)) return [];
13646
13906
  const cutoff = now - retentionDays * DAY_MS2;
13647
- return readdirSync7(dir).map((name) => join24(dir, name)).filter((path) => {
13907
+ return readdirSync7(dir).map((name) => join25(dir, name)).filter((path) => {
13648
13908
  try {
13649
13909
  return statSync8(path).mtimeMs < cutoff;
13650
13910
  } catch {
@@ -13672,7 +13932,7 @@ function applyStreamSweep(paths, dryRun, log) {
13672
13932
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13673
13933
  if (!existsSync24(stateDir)) return [];
13674
13934
  const cutoff = now - retentionDays * DAY_MS2;
13675
- return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join24(stateDir, name)).filter((path) => {
13935
+ return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join25(stateDir, name)).filter((path) => {
13676
13936
  try {
13677
13937
  return statSync8(path).mtimeMs < cutoff;
13678
13938
  } catch {
@@ -13682,26 +13942,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13682
13942
  }
13683
13943
 
13684
13944
  // src/cli.ts
13685
- import { realpathSync as realpathSync2, readFileSync as readFileSync24, existsSync as existsSync31, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13945
+ import { realpathSync as realpathSync3, readFileSync as readFileSync25, existsSync as existsSync31, mkdirSync as mkdirSync18, writeFileSync as writeFileSync19, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13686
13946
  import { createInterface } from "readline/promises";
13687
13947
  import { dirname as dirOf, resolve as resolvePath } from "path";
13688
13948
 
13689
13949
  // src/ssh-keys.ts
13690
- import { execFileSync as execFileSync4 } from "child_process";
13691
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, chmodSync } from "fs";
13692
- import { dirname as dirname16, join as join25 } from "path";
13950
+ import { execFileSync as execFileSync5 } from "child_process";
13951
+ import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as readFileSync19, chmodSync } from "fs";
13952
+ import { dirname as dirname17, join as join26 } from "path";
13693
13953
  var SshKeyError = class extends Error {
13694
13954
  };
13695
13955
  function shipSshKeyPathFor(stateDir) {
13696
- return join25(stateDir, "ssh", "id_ed25519");
13956
+ return join26(stateDir, "ssh", "id_ed25519");
13697
13957
  }
13698
13958
  function ensureShipSshKeypair(stateDir) {
13699
13959
  const privateKeyPath = shipSshKeyPathFor(stateDir);
13700
13960
  const publicKeyPath = `${privateKeyPath}.pub`;
13701
13961
  if (!existsSync25(privateKeyPath)) {
13702
- mkdirSync12(dirname16(privateKeyPath), { recursive: true });
13962
+ mkdirSync13(dirname17(privateKeyPath), { recursive: true });
13703
13963
  try {
13704
- execFileSync4("ssh-keygen", [
13964
+ execFileSync5("ssh-keygen", [
13705
13965
  "-t",
13706
13966
  "ed25519",
13707
13967
  "-N",
@@ -13722,12 +13982,12 @@ function ensureShipSshKeypair(stateDir) {
13722
13982
  if (!existsSync25(publicKeyPath)) {
13723
13983
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
13724
13984
  }
13725
- const publicKey = readFileSync18(publicKeyPath, "utf8").trim();
13985
+ const publicKey = readFileSync19(publicKeyPath, "utf8").trim();
13726
13986
  return { privateKeyPath, publicKeyPath, publicKey };
13727
13987
  }
13728
13988
  function sshKeygenAvailable() {
13729
13989
  try {
13730
- execFileSync4("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
13990
+ execFileSync5("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
13731
13991
  return true;
13732
13992
  } catch (e) {
13733
13993
  return e.code !== "ENOENT";
@@ -13735,18 +13995,18 @@ function sshKeygenAvailable() {
13735
13995
  }
13736
13996
 
13737
13997
  // src/machine-id.ts
13738
- import { execFileSync as execFileSync5 } from "child_process";
13998
+ import { execFileSync as execFileSync6 } from "child_process";
13739
13999
  import { createHash as createHash4 } from "crypto";
13740
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
13741
- import { dirname as dirname17, join as join26 } from "path";
14000
+ import { existsSync as existsSync26, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "fs";
14001
+ import { dirname as dirname18, join as join27 } from "path";
13742
14002
  function machineIdPathFor(stateDir) {
13743
- return join26(stateDir, "machine-id");
14003
+ return join27(stateDir, "machine-id");
13744
14004
  }
13745
14005
  var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
13746
14006
  function readRawMachineId(platform2 = process.platform) {
13747
14007
  try {
13748
14008
  if (platform2 === "darwin") {
13749
- const out = execFileSync5("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
14009
+ const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
13750
14010
  encoding: "utf8",
13751
14011
  stdio: ["ignore", "pipe", "ignore"]
13752
14012
  });
@@ -13754,7 +14014,7 @@ function readRawMachineId(platform2 = process.platform) {
13754
14014
  }
13755
14015
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
13756
14016
  if (existsSync26(p)) {
13757
- const id = readFileSync19(p, "utf8").trim();
14017
+ const id = readFileSync20(p, "utf8").trim();
13758
14018
  if (id) return id;
13759
14019
  }
13760
14020
  }
@@ -13766,14 +14026,14 @@ function ensureMachineId(stateDir, opts = {}) {
13766
14026
  if (opts.override?.trim()) return sha256(opts.override.trim());
13767
14027
  const path = machineIdPathFor(stateDir);
13768
14028
  if (existsSync26(path)) {
13769
- const cached = readFileSync19(path, "utf8").trim();
14029
+ const cached = readFileSync20(path, "utf8").trim();
13770
14030
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
13771
14031
  }
13772
14032
  const raw = (opts.read ?? readRawMachineId)();
13773
14033
  if (!raw) return void 0;
13774
14034
  const hashed = sha256(raw);
13775
- mkdirSync13(dirname17(path), { recursive: true });
13776
- writeFileSync14(path, `${hashed}
14035
+ mkdirSync14(dirname18(path), { recursive: true });
14036
+ writeFileSync15(path, `${hashed}
13777
14037
  `, { mode: 384 });
13778
14038
  return hashed;
13779
14039
  }
@@ -13785,11 +14045,11 @@ function matchShipRows(ships, shipName2, machineId) {
13785
14045
  }
13786
14046
 
13787
14047
  // src/passenger-containers.ts
13788
- import { execFileSync as execFileSync6 } from "child_process";
14048
+ import { execFileSync as execFileSync7 } from "child_process";
13789
14049
  import { createHash as createHash5 } from "crypto";
13790
14050
  var realExec = (cmd, args) => {
13791
14051
  try {
13792
- return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
14052
+ return execFileSync7(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13793
14053
  } catch (e) {
13794
14054
  const err = e;
13795
14055
  const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -13987,12 +14247,12 @@ function syncAllPassengerCheckouts(cfg) {
13987
14247
  }
13988
14248
 
13989
14249
  // src/tunnel.ts
13990
- import { spawn as spawn4 } from "child_process";
13991
- import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
14250
+ import { spawn as spawn5 } from "child_process";
14251
+ import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
13992
14252
  import { randomInt } from "crypto";
13993
- import { join as join27 } from "path";
14253
+ import { join as join28 } from "path";
13994
14254
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
13995
- var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
14255
+ var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
13996
14256
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
13997
14257
  var SLUG_LENGTH = 12;
13998
14258
  function generateTunnelSlug() {
@@ -14057,21 +14317,21 @@ function startTunnel(opts) {
14057
14317
  };
14058
14318
  }
14059
14319
  function tunnelStatePath(stateDir, workspaceId) {
14060
- return join27(stateDir, "passengers", `${workspaceId}.json`);
14320
+ return join28(stateDir, "passengers", `${workspaceId}.json`);
14061
14321
  }
14062
14322
  function readPersistedTunnel(stateDir, workspaceId) {
14063
14323
  const path = tunnelStatePath(stateDir, workspaceId);
14064
14324
  if (!existsSync27(path)) return void 0;
14065
14325
  try {
14066
- return JSON.parse(readFileSync20(path, "utf8"));
14326
+ return JSON.parse(readFileSync21(path, "utf8"));
14067
14327
  } catch {
14068
14328
  return void 0;
14069
14329
  }
14070
14330
  }
14071
14331
  function writePersistedTunnel(stateDir, state) {
14072
14332
  const path = tunnelStatePath(stateDir, state.workspaceId);
14073
- mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14074
- writeFileSync15(path, `${JSON.stringify(state, null, 2)}
14333
+ mkdirSync15(join28(stateDir, "passengers"), { recursive: true });
14334
+ writeFileSync16(path, `${JSON.stringify(state, null, 2)}
14075
14335
  `);
14076
14336
  }
14077
14337
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -14168,7 +14428,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
14168
14428
  }
14169
14429
  }
14170
14430
  function readAllPersistedWorkspaceIds(stateDir) {
14171
- const dir = join27(stateDir, "passengers");
14431
+ const dir = join28(stateDir, "passengers");
14172
14432
  if (!existsSync27(dir)) return [];
14173
14433
  return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14174
14434
  }
@@ -14288,10 +14548,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
14288
14548
 
14289
14549
  // src/passenger-sync-daemon.ts
14290
14550
  import { createServer as createHttpServer } from "http";
14291
- import { spawn as spawn5 } from "child_process";
14551
+ import { spawn as spawn6 } from "child_process";
14292
14552
  import { randomBytes } from "crypto";
14293
- import { existsSync as existsSync28, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14294
- import { join as join28 } from "path";
14553
+ import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14554
+ import { join as join29 } from "path";
14295
14555
 
14296
14556
  // src/secret-compare.ts
14297
14557
  import { timingSafeEqual } from "crypto";
@@ -14375,26 +14635,26 @@ function runSyncDaemonFromEnv(env = process.env) {
14375
14635
  return server;
14376
14636
  }
14377
14637
  var defaultSpawn2 = (cmd, args, env) => {
14378
- const child = spawn5(cmd, args, { detached: true, stdio: "ignore", env });
14638
+ const child = spawn6(cmd, args, { detached: true, stdio: "ignore", env });
14379
14639
  child.unref();
14380
14640
  return child;
14381
14641
  };
14382
14642
  function syncDaemonStatePath(stateDir, workspaceId) {
14383
- return join28(stateDir, "passengers", `${workspaceId}-sync.json`);
14643
+ return join29(stateDir, "passengers", `${workspaceId}-sync.json`);
14384
14644
  }
14385
14645
  function readPersistedSyncDaemon(stateDir, workspaceId) {
14386
14646
  const path = syncDaemonStatePath(stateDir, workspaceId);
14387
14647
  if (!existsSync28(path)) return void 0;
14388
14648
  try {
14389
- return JSON.parse(readFileSync21(path, "utf8"));
14649
+ return JSON.parse(readFileSync22(path, "utf8"));
14390
14650
  } catch {
14391
14651
  return void 0;
14392
14652
  }
14393
14653
  }
14394
14654
  function writePersistedSyncDaemon(stateDir, state) {
14395
14655
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
14396
- mkdirSync15(join28(stateDir, "passengers"), { recursive: true });
14397
- writeFileSync16(path, `${JSON.stringify(state, null, 2)}
14656
+ mkdirSync16(join29(stateDir, "passengers"), { recursive: true });
14657
+ writeFileSync17(path, `${JSON.stringify(state, null, 2)}
14398
14658
  `);
14399
14659
  }
14400
14660
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -14414,7 +14674,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
14414
14674
  }
14415
14675
  }
14416
14676
  function readAllPersistedSyncWorkspaceIds(stateDir) {
14417
- const dir = join28(stateDir, "passengers");
14677
+ const dir = join29(stateDir, "passengers");
14418
14678
  if (!existsSync28(dir)) return [];
14419
14679
  const suffix = "-sync.json";
14420
14680
  return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -14472,12 +14732,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14472
14732
  }
14473
14733
 
14474
14734
  // src/daemon.ts
14475
- import { existsSync as existsSync30, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync17 } from "fs";
14476
- import { join as join30 } from "path";
14735
+ import { existsSync as existsSync30, mkdirSync as mkdirSync17, readFileSync as readFileSync24, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync18 } from "fs";
14736
+ import { join as join31 } from "path";
14477
14737
 
14478
14738
  // src/interrupted.ts
14479
- import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
14480
- import { join as join29 } from "path";
14739
+ import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
14740
+ import { join as join30 } from "path";
14481
14741
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
14482
14742
  function interruptedComment(b) {
14483
14743
  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.";
@@ -14498,19 +14758,19 @@ function runFinished(streamPath) {
14498
14758
  const path = `${streamPath}.meta.json`;
14499
14759
  if (!existsSync29(path)) return false;
14500
14760
  try {
14501
- return JSON.parse(readFileSync22(path, "utf8")).running !== true;
14761
+ return JSON.parse(readFileSync23(path, "utf8")).running !== true;
14502
14762
  } catch {
14503
14763
  return true;
14504
14764
  }
14505
14765
  }
14506
14766
  function orphanedStreams(o) {
14507
- const dir = join29(o.stateDir, "streams");
14767
+ const dir = join30(o.stateDir, "streams");
14508
14768
  if (!existsSync29(dir)) return [];
14509
14769
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
14510
14770
  const out = [];
14511
14771
  for (const name of readdirSync10(dir)) {
14512
14772
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14513
- const streamPath = join29(dir, name);
14773
+ const streamPath = join30(dir, name);
14514
14774
  if (runFinished(streamPath)) continue;
14515
14775
  const sidecar = readRunSidecar(streamPath);
14516
14776
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -14610,7 +14870,7 @@ function latestMtime(path) {
14610
14870
  if (!stat.isDirectory()) return 0;
14611
14871
  let latest = stat.mtimeMs;
14612
14872
  for (const entry of readdirSync11(path, { withFileTypes: true })) {
14613
- latest = Math.max(latest, latestMtime(join30(path, entry.name)));
14873
+ latest = Math.max(latest, latestMtime(join31(path, entry.name)));
14614
14874
  }
14615
14875
  return latest;
14616
14876
  }
@@ -14622,20 +14882,20 @@ function makeStaleChecker(paths, opts = {}) {
14622
14882
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync30(opts.reloadSentinel);
14623
14883
  }
14624
14884
  function reloadSentinelFor(stateDir) {
14625
- return join30(stateDir, "reload-requested");
14885
+ return join31(stateDir, "reload-requested");
14626
14886
  }
14627
14887
  function daemonStampFor(stateDir) {
14628
- return join30(stateDir, "daemon.started");
14888
+ return join31(stateDir, "daemon.started");
14629
14889
  }
14630
14890
  function noteDaemonStarted(stateDir, now = Date.now()) {
14631
- mkdirSync16(stateDir, { recursive: true });
14891
+ mkdirSync17(stateDir, { recursive: true });
14632
14892
  rmSync11(reloadSentinelFor(stateDir), { force: true });
14633
- writeFileSync17(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14893
+ writeFileSync18(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14634
14894
  `);
14635
14895
  }
14636
14896
  function readDaemonStarted(stateDir) {
14637
14897
  try {
14638
- const v = JSON.parse(readFileSync23(daemonStampFor(stateDir), "utf8")).startedAt;
14898
+ const v = JSON.parse(readFileSync24(daemonStampFor(stateDir), "utf8")).startedAt;
14639
14899
  return typeof v === "number" ? v : null;
14640
14900
  } catch {
14641
14901
  return null;
@@ -14695,7 +14955,7 @@ async function runOnePassUnguarded(o) {
14695
14955
  await hydrateApiKeys([o.route], o.getSessionStore);
14696
14956
  const emit = o.newEmitter();
14697
14957
  const decide = o.decide ?? decideCycle;
14698
- const spawn6 = o.spawnRoleAgent ?? runRoleAgent;
14958
+ const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
14699
14959
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
14700
14960
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
14701
14961
  try {
@@ -14756,7 +15016,7 @@ async function runOnePassUnguarded(o) {
14756
15016
  continue;
14757
15017
  }
14758
15018
  started.push(role);
14759
- const run2 = spawn6(role, decision, ctx).then(() => {
15019
+ const run2 = spawn7(role, decision, ctx).then(() => {
14760
15020
  breaker.recordSuccess(role);
14761
15021
  clearShipAttention(o.state, `role_parked:${role}`);
14762
15022
  }).catch(async (e) => {
@@ -15001,7 +15261,7 @@ async function buildEnvironment(route, ship, ticket) {
15001
15261
  }
15002
15262
 
15003
15263
  // src/cli.ts
15004
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname18(process.execPath) : resolve5(dirname18(fileURLToPath(import.meta.url)), "..");
15264
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname19(process.execPath) : resolve5(dirname19(fileURLToPath(import.meta.url)), "..");
15005
15265
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
15006
15266
  var CYCLE_SECONDS2 = 120;
15007
15267
  var STALE_CYCLES = 3;
@@ -15147,7 +15407,7 @@ function usage() {
15147
15407
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
15148
15408
  crew log [route] tail the log
15149
15409
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
15150
- crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one; --dry-run works with no crew.yaml and writes none: no file, no rows, no seat keys \u2014 the device approval and workspace credential behave as in a real run)
15410
+ crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one; --dry-run works with no crew.yaml and writes none: no file, no rows, no seat keys \u2014 the device approval and workspace credential behave as in a real run; with no workspace named it first reuses the keychain session a previous dry-run stored for the host, falling back to device approval when that is missing or rejected, and the signed_in event's session field says which)
15151
15411
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
15152
15412
  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)
15153
15413
  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)
@@ -15155,7 +15415,7 @@ function usage() {
15155
15415
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
15156
15416
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
15157
15417
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
15158
- crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo, --json)
15418
+ crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
15159
15419
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
15160
15420
  crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
15161
15421
  crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
@@ -15568,7 +15828,7 @@ if (command === "passenger-sync-daemon") {
15568
15828
  }
15569
15829
  if (host === "linux" && hasSystemd) {
15570
15830
  const label = labelFor(CREW_HOME, "run");
15571
- const staleTimer = join31(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15831
+ const staleTimer = join32(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15572
15832
  if (existsSync31(staleTimer)) {
15573
15833
  process.stdout.write(
15574
15834
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -15630,7 +15890,7 @@ if (command === "passenger-sync-daemon") {
15630
15890
  for (const a of plan.actions) {
15631
15891
  if (a.kind === "enable") {
15632
15892
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
15633
- writeFileSync18(cfg.configFile, planEnable(readFileSync24(cfg.configFile, "utf8"), a.route));
15893
+ writeFileSync19(cfg.configFile, planEnable(readFileSync25(cfg.configFile, "utf8"), a.route));
15634
15894
  enabledSound.add(a.route);
15635
15895
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
15636
15896
  } else {
@@ -16084,10 +16344,12 @@ ${changed2.map((c) => ` - ${c}
16084
16344
  lock?.release();
16085
16345
  lock = void 0;
16086
16346
  };
16347
+ let sigintOwnedByCommand = false;
16087
16348
  if (!dryRun) {
16088
16349
  process.on("exit", dropLock);
16089
16350
  for (const sig of ["SIGINT", "SIGTERM"]) {
16090
16351
  process.on(sig, () => {
16352
+ if (sig === "SIGINT" && sigintOwnedByCommand) return;
16091
16353
  dropLock();
16092
16354
  process.exit(0);
16093
16355
  });
@@ -16629,7 +16891,7 @@ ${changed2.map((c) => ` - ${c}
16629
16891
  process.exit(1);
16630
16892
  }
16631
16893
  requireArmed("run the daemon loop");
16632
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join31(CREW_HOME, "src"), join31(CREW_HOME, "dist"), join31(CREW_HOME, "bin")];
16894
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join32(CREW_HOME, "src"), join32(CREW_HOME, "dist"), join32(CREW_HOME, "bin")];
16633
16895
  const configPaths = daemonConfigPaths(cfg);
16634
16896
  watchPaths.push(...configPaths);
16635
16897
  noteDaemonStarted(cfg.ship.stateDir);
@@ -16830,10 +17092,10 @@ ${changed2.map((c) => ` - ${c}
16830
17092
  `);
16831
17093
  if (!dryRun) {
16832
17094
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16833
- const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync24(resolvedPath, "utf8")) : {};
17095
+ const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync25(resolvedPath, "utf8")) : {};
16834
17096
  raw.agentSkills = result.agentSkills;
16835
- mkdirSync17(dirOf(resolvedPath), { recursive: true });
16836
- writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
17097
+ mkdirSync18(dirOf(resolvedPath), { recursive: true });
17098
+ writeFileSync19(resolvedPath, `${JSON.stringify(raw, null, 2)}
16837
17099
  `);
16838
17100
  }
16839
17101
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16842,19 +17104,25 @@ ${changed2.map((c) => ` - ${c}
16842
17104
  case "repos": {
16843
17105
  const sub = positional[1];
16844
17106
  const usage2 = () => {
16845
- 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");
17107
+ 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] [--agent] [--write route|repo] [--recipes ID,ID] [--json]\n");
16846
17108
  process.exit(2);
16847
17109
  };
16848
17110
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
16849
- const configText = readFileSync24(cfg.configFile, "utf8");
17111
+ const configText = readFileSync25(cfg.configFile, "utf8");
16850
17112
  if (sub === "hooks") {
16851
17113
  try {
16852
17114
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
16853
- const dir = repoDirOf(configText, dirname18(cfg.configFile), a.route, a.name);
17115
+ const dir = repoDirOf(configText, dirname19(cfg.configFile), a.route, a.name);
16854
17116
  if (!existsSync31(dir)) throw new ReposError(`${dir} does not exist on disk`);
16855
- const files = { exists: (r) => existsSync31(resolvePath(dir, r)), read: (r) => {
17117
+ const files = { exists: (r) => existsSync31(resolvePath(dir, r)), list: (r) => {
17118
+ try {
17119
+ return readdirSync12(resolvePath(dir, r));
17120
+ } catch {
17121
+ return [];
17122
+ }
17123
+ }, read: (r) => {
16856
17124
  try {
16857
- return readFileSync24(resolvePath(dir, r), "utf8");
17125
+ return readFileSync25(resolvePath(dir, r), "utf8");
16858
17126
  } catch {
16859
17127
  return void 0;
16860
17128
  }
@@ -16862,21 +17130,43 @@ ${changed2.map((c) => ` - ${c}
16862
17130
  const suggestion = suggestHooks(files);
16863
17131
  const write = value("write");
16864
17132
  if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
17133
+ const recipeIds = (value("recipes") ?? "").split(",").map((x) => x.trim()).filter((x) => x && x !== "none");
17134
+ if (recipeIds.length > 0 && !write) throw new ReposError("--recipes only applies with --write route|repo");
17135
+ if (flag("agent") && write) throw new ReposError("--agent is read-only; drop --write (write the sniffed hooks, then re-run --agent to compare)");
17136
+ let agent = null;
17137
+ let agentError = null;
17138
+ if (flag("agent")) {
17139
+ const ac = new AbortController();
17140
+ const onSigint = () => ac.abort();
17141
+ process.once("SIGINT", onSigint);
17142
+ sigintOwnedByCommand = true;
17143
+ try {
17144
+ const routeCfg = cfg.routes.find((r) => r.route === a.route);
17145
+ agent = await runAgentSuggest({ spec: resolveToolchain(cfg.ship, routeCfg, a.name), dir, sniff: suggestion, signal: ac.signal });
17146
+ } catch (e) {
17147
+ if (!(e instanceof AgentSuggestError)) throw e;
17148
+ agentError = { reason: e.reason, message: e.message };
17149
+ } finally {
17150
+ process.off("SIGINT", onSigint);
17151
+ sigintOwnedByCommand = false;
17152
+ }
17153
+ }
17154
+ const picked = selectRecipes(suggestion, recipeIds);
16865
17155
  let written;
16866
- if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
17156
+ if (write && suggestion.source === null && recipeIds.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
16867
17157
  if (write === "route") {
16868
- const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
16869
- writeFileSync18(cfg.configFile, plan.text);
17158
+ const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
17159
+ writeFileSync19(cfg.configFile, plan.text);
16870
17160
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
16871
17161
  } else if (write === "repo") {
16872
17162
  const have = existingRepoConfig(files);
16873
17163
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
16874
17164
  const file = resolvePath(dir, ".crew.yaml");
16875
- writeFileSync18(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16876
- written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
17165
+ writeFileSync19(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
17166
+ written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
16877
17167
  }
16878
17168
  if (flag("json")) {
16879
- process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, written: written ?? null })}
17169
+ process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, agent, agentError, written: written ?? null })}
16880
17170
  `);
16881
17171
  } else {
16882
17172
  if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
@@ -16884,6 +17174,18 @@ ${changed2.map((c) => ` - ${c}
16884
17174
  for (const [k, v] of Object.entries(suggestion.hooks)) process.stdout.write(` ${k}: ${v}
16885
17175
  `);
16886
17176
  for (const n of suggestion.notes) process.stdout.write(`note: ${n}
17177
+ `);
17178
+ for (const r of suggestion.recipes) process.stdout.write(`recipe ${r.id}${r.defaultOn ? " (default on)" : ""}${r.writable ? "" : " (advisory)"}: ${r.title} [${r.evidence.join(", ")}]
17179
+ `);
17180
+ if (agent) {
17181
+ process.stdout.write(`agent (${agent.toolchain}, ${agent.model}) read ${agent.filesRead.join(", ") || "no files"}
17182
+ `);
17183
+ for (const [k, v] of Object.entries(agent.hooks)) process.stdout.write(` ${k}: ${v}${agent.differsFromSniff.includes(k) ? " (differs from file sniff)" : ""}
17184
+ `);
17185
+ for (const n of agent.notes) process.stdout.write(`note: ${n}
17186
+ `);
17187
+ }
17188
+ if (agentError) process.stderr.write(`crew repos: --agent failed (${agentError.reason}): ${agentError.message}
16887
17189
  `);
16888
17190
  if (written) {
16889
17191
  process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
@@ -16891,8 +17193,11 @@ ${changed2.map((c) => ` - ${c}
16891
17193
  if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
16892
17194
  `);
16893
17195
  if (written.target === "repo") process.stdout.write("review it and commit it to the repository\n");
17196
+ if (written.target === "route" && picked.release) process.stdout.write(`note: release.mode ${picked.release.mode} is repo-level; use --write repo to set it
17197
+ `);
16894
17198
  }
16895
17199
  }
17200
+ if (agentError) process.exitCode = 1;
16896
17201
  } catch (e) {
16897
17202
  if (e instanceof ReposError) {
16898
17203
  process.stderr.write(`crew repos: ${e.message}
@@ -16940,13 +17245,13 @@ ${changed2.map((c) => ` - ${c}
16940
17245
  let resolvedRemotes;
16941
17246
  if (existsSync31(resolvedFile)) {
16942
17247
  try {
16943
- resolvedRemotes = JSON.parse(readFileSync24(resolvedFile, "utf8"));
17248
+ resolvedRemotes = JSON.parse(readFileSync25(resolvedFile, "utf8"));
16944
17249
  } catch {
16945
17250
  }
16946
17251
  }
16947
17252
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16948
17253
  const text = configText;
16949
- const base = dirname18(cfg.configFile);
17254
+ const base = dirname19(cfg.configFile);
16950
17255
  try {
16951
17256
  if (sub === "list") {
16952
17257
  const repos = configuredRepos(text, routeName, base);
@@ -16987,7 +17292,7 @@ ${changed2.map((c) => ` - ${c}
16987
17292
  throw new ReposError(`${e.message}${authHint}
16988
17293
  crew.yaml was not changed`);
16989
17294
  }
16990
- writeFileSync18(cfg.configFile, plan2.text);
17295
+ writeFileSync19(cfg.configFile, plan2.text);
16991
17296
  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}`);
16992
17297
  break;
16993
17298
  }
@@ -17042,14 +17347,14 @@ ${changed2.map((c) => ` - ${c}
17042
17347
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
17043
17348
  `);
17044
17349
  if (existsSync31(resolvedFile)) {
17045
- const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
17350
+ const raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
17046
17351
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
17047
17352
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
17048
- writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
17353
+ writeFileSync19(resolvedFile, `${JSON.stringify(raw, null, 2)}
17049
17354
  `);
17050
17355
  }
17051
17356
  }
17052
- writeFileSync18(cfg.configFile, plan.text);
17357
+ writeFileSync19(cfg.configFile, plan.text);
17053
17358
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
17054
17359
  `);
17055
17360
  } catch (e) {
@@ -17179,6 +17484,30 @@ ${reconstruction.prompt}
17179
17484
  } catch {
17180
17485
  }
17181
17486
  }
17487
+ let signInSource = apiKey ? "key" : "device";
17488
+ if (!apiKey && !workspace && dryRun && store) {
17489
+ const resumed = await resumeKeychainSession({
17490
+ store,
17491
+ stateDir: cfg.ship.stateDir,
17492
+ host: hostFromUrl3(baseUrl),
17493
+ verify: async (k) => {
17494
+ try {
17495
+ await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
17496
+ return true;
17497
+ } catch {
17498
+ return false;
17499
+ }
17500
+ }
17501
+ });
17502
+ if (resumed) {
17503
+ apiKey = resumed.apiKey;
17504
+ workspace = resumed.workspace;
17505
+ keychainBacked = true;
17506
+ signInSource = "keychain";
17507
+ process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl3(baseUrl)}/${workspace}.
17508
+ `);
17509
+ }
17510
+ }
17182
17511
  let mintedFromDevice = false;
17183
17512
  let signedInEmitted = false;
17184
17513
  if (!apiKey) {
@@ -17231,7 +17560,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17231
17560
  signedInEmitted = true;
17232
17561
  connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
17233
17562
  identity: { id: result.identity.id, email: result.identity.email },
17234
- workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }]
17563
+ workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
17564
+ session: "device"
17235
17565
  });
17236
17566
  const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
17237
17567
  if (!workspace) workspace = deviceWorkspace;
@@ -17245,6 +17575,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17245
17575
  createdAt: result.apiKey.createdAt
17246
17576
  });
17247
17577
  keychainBacked = true;
17578
+ rememberSession(cfg.ship.stateDir, hostFromUrl3(baseUrl), deviceWorkspace);
17248
17579
  process.stderr.write(
17249
17580
  `API key stored in the OS keychain for ${hostFromUrl3(baseUrl)}/${deviceWorkspace}.
17250
17581
  `
@@ -17290,7 +17621,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17290
17621
  signedInEmitted = true;
17291
17622
  connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
17292
17623
  identity: { id: found.meId, email: found.meEmail, name: found.meName },
17293
- workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }]
17624
+ workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
17625
+ session: signInSource
17294
17626
  });
17295
17627
  }
17296
17628
  connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
@@ -17380,7 +17712,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17380
17712
  const hadPreviousFile = existsSync31(resolvedPath);
17381
17713
  if (hadPreviousFile) {
17382
17714
  try {
17383
- previous = JSON.parse(readFileSync24(resolvedPath, "utf8"));
17715
+ previous = JSON.parse(readFileSync25(resolvedPath, "utf8"));
17384
17716
  } catch {
17385
17717
  }
17386
17718
  }
@@ -17489,10 +17821,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17489
17821
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
17490
17822
  `);
17491
17823
  } else {
17492
- mkdirSync17(dirname18(resolvedPath), { recursive: true });
17824
+ mkdirSync18(dirname19(resolvedPath), { recursive: true });
17493
17825
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
17494
17826
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
17495
- writeFileSync18(tmpPath, `${JSON.stringify(resolved, null, 2)}
17827
+ writeFileSync19(tmpPath, `${JSON.stringify(resolved, null, 2)}
17496
17828
  `);
17497
17829
  renameSync2(tmpPath, resolvedPath);
17498
17830
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -17527,8 +17859,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17527
17859
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
17528
17860
  `);
17529
17861
  } else {
17530
- mkdirSync17(dirname18(apiKeyFile), { recursive: true });
17531
- writeFileSync18(apiKeyFile, `${apiKeyVar}=${apiKey}
17862
+ mkdirSync18(dirname19(apiKeyFile), { recursive: true });
17863
+ writeFileSync19(apiKeyFile, `${apiKeyVar}=${apiKey}
17532
17864
  `, { mode: 384 });
17533
17865
  process.stderr.write(`API key written to ${apiKeyFile}
17534
17866
  `);
@@ -17537,7 +17869,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17537
17869
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
17538
17870
  const wizard = firstRun;
17539
17871
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
17540
- const routeDir = value("dir") ?? (writeFirstRun ? join31(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17872
+ const routeDir = value("dir") ?? (writeFirstRun ? join32(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17541
17873
  const routeBlock = renderConnection(
17542
17874
  found,
17543
17875
  route2,
@@ -18175,13 +18507,13 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18175
18507
  },
18176
18508
  inspectCheckout(dir) {
18177
18509
  const dirty = status(dir).map((c) => c.path);
18178
- const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync2(p) !== realpathSync2(dir));
18510
+ const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
18179
18511
  return { dirty, linkedWorktrees };
18180
18512
  }
18181
18513
  };
18182
18514
  const droppedRoutes = [];
18183
18515
  try {
18184
- const rawCfg = parseYaml2(readFileSync24(cfg.configFile, "utf8"));
18516
+ const rawCfg = parseYaml2(readFileSync25(cfg.configFile, "utf8"));
18185
18517
  for (const r of rawCfg?.routes ?? []) {
18186
18518
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
18187
18519
  try {
@@ -18223,8 +18555,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18223
18555
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
18224
18556
  break;
18225
18557
  }
18226
- mkdirSync17(cfg.ship.stateDir, { recursive: true });
18227
- writeFileSync18(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18558
+ mkdirSync18(cfg.ship.stateDir, { recursive: true });
18559
+ writeFileSync19(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18228
18560
  `);
18229
18561
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
18230
18562
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -18275,7 +18607,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18275
18607
  }
18276
18608
  case "log": {
18277
18609
  try {
18278
- const text = readFileSync24(cfg.ship.logFile, "utf8").split("\n");
18610
+ const text = readFileSync25(cfg.ship.logFile, "utf8").split("\n");
18279
18611
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
18280
18612
  `);
18281
18613
  } catch {
@@ -18298,8 +18630,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18298
18630
  };
18299
18631
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
18300
18632
  const tracker = new Tracker(route, cfg.ship);
18301
- const configText = readFileSync24(cfg.configFile, "utf8");
18302
- const cfgBase = dirname18(cfg.configFile);
18633
+ const configText = readFileSync25(cfg.configFile, "utf8");
18634
+ const cfgBase = dirname19(cfg.configFile);
18303
18635
  const configured = configuredRepos(configText, route.route, cfgBase);
18304
18636
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
18305
18637
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -18327,7 +18659,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18327
18659
  }
18328
18660
  if (configured.some((r) => r.dir === dir)) {
18329
18661
  try {
18330
- writeFileSync18(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18662
+ writeFileSync19(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18331
18663
  } catch (e) {
18332
18664
  if (e instanceof ReposError) fail(e.message);
18333
18665
  throw e;
@@ -18336,28 +18668,28 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18336
18668
  if (row) {
18337
18669
  await tracker.deleteRepoRow(row.id);
18338
18670
  if (existsSync31(resolvedFile)) {
18339
- const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
18671
+ const raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
18340
18672
  if (raw.repoNames) delete raw.repoNames[row.id];
18341
18673
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
18342
- writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
18674
+ writeFileSync19(resolvedFile, `${JSON.stringify(raw, null, 2)}
18343
18675
  `);
18344
18676
  }
18345
18677
  }
18346
- if (existsSync31(join31(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
18678
+ if (existsSync31(join32(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
18347
18679
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
18348
18680
  break;
18349
18681
  }
18350
18682
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
18351
- const fresh = !existsSync31(join31(dir, ".git"));
18683
+ const fresh = !existsSync31(join32(dir, ".git"));
18352
18684
  if (existsSync31(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
18353
18685
  if (dryRun) {
18354
18686
  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`);
18355
18687
  break;
18356
18688
  }
18357
18689
  if (fresh) {
18358
- mkdirSync17(dir, { recursive: true });
18359
- writeFileSync18(join31(dir, "README.md"), SAMPLE_README);
18360
- writeFileSync18(join31(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18690
+ mkdirSync18(dir, { recursive: true });
18691
+ writeFileSync19(join32(dir, "README.md"), SAMPLE_README);
18692
+ writeFileSync19(join32(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18361
18693
  const g = (...a) => {
18362
18694
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
18363
18695
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -18376,16 +18708,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18376
18708
  });
18377
18709
  repoId = (await tracker.createRepoRow(payload)).id;
18378
18710
  if (existsSync31(resolvedFile)) {
18379
- const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
18711
+ const raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
18380
18712
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
18381
18713
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
18382
- writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
18714
+ writeFileSync19(resolvedFile, `${JSON.stringify(raw, null, 2)}
18383
18715
  `);
18384
18716
  }
18385
18717
  }
18386
18718
  if (!configured.some((r) => r.dir === dir)) {
18387
18719
  try {
18388
- writeFileSync18(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18720
+ writeFileSync19(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18389
18721
  } catch (e) {
18390
18722
  if (e instanceof ReposError) fail(e.message);
18391
18723
  throw e;
@@ -18718,7 +19050,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18718
19050
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
18719
19051
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
18720
19052
  try {
18721
- return readFileSync24(p, "utf8");
19053
+ return readFileSync25(p, "utf8");
18722
19054
  } catch {
18723
19055
  return null;
18724
19056
  }