@tablation/crew 0.92.0 → 0.94.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 (2) hide show
  1. package/dist/cli.js +1067 -621
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -223,8 +223,8 @@ function shellInitScript(shell) {
223
223
  }
224
224
 
225
225
  // src/config.ts
226
- import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
227
- import { resolve, dirname as dirname4, isAbsolute, join as join11, basename as basename3 } from "path";
226
+ import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
227
+ import { resolve, dirname as dirname4, isAbsolute, join as join12, basename as basename3 } from "path";
228
228
  import { homedir as homedir6, tmpdir } from "os";
229
229
  import { parse as parse2 } from "yaml";
230
230
 
@@ -363,11 +363,11 @@ var Emitter = class _Emitter {
363
363
  const result = await fn();
364
364
  this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
365
365
  return result;
366
- } catch (err2) {
367
- this.error(`${message} \u2014 ${err2.message}`, {
366
+ } catch (err3) {
367
+ this.error(`${message} \u2014 ${err3.message}`, {
368
368
  data: { ms: this.now().getTime() - started, ok: false }
369
369
  });
370
- throw err2;
370
+ throw err3;
371
371
  }
372
372
  }
373
373
  };
@@ -1818,7 +1818,144 @@ function forgeOfRemote(forges, remoteUrl, hasToken) {
1818
1818
  }
1819
1819
 
1820
1820
  // src/config.ts
1821
- import { getSessionStore, hostFromUrl } from "@tablation/client";
1821
+ import { hostFromUrl } from "@tablation/client";
1822
+
1823
+ // src/secret-store.ts
1824
+ import { createHash as createHash2 } from "crypto";
1825
+ import { accessSync, constants, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync6, statSync as statSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync2 } from "fs";
1826
+ import { join as join10 } from "path";
1827
+ import { getSessionStore as getOsSessionStore } from "@tablation/client";
1828
+ var SECRET_STORE_MODES = ["auto", "os", "file"];
1829
+ function isSecretStoreMode(v) {
1830
+ return typeof v === "string" && SECRET_STORE_MODES.includes(v);
1831
+ }
1832
+ var configured;
1833
+ function configureSecretStore(c) {
1834
+ configured = c;
1835
+ }
1836
+ function keysDir(stateDir) {
1837
+ return join10(stateDir, "keys");
1838
+ }
1839
+ function entryName(host, slug) {
1840
+ return `${createHash2("sha256").update(`${host}\0${slug}`).digest("hex")}.json`;
1841
+ }
1842
+ var FileSessionStore = class {
1843
+ dir;
1844
+ constructor(dir) {
1845
+ this.dir = dir;
1846
+ }
1847
+ ensureDir() {
1848
+ mkdirSync6(this.dir, { recursive: true, mode: 448 });
1849
+ if ((statSync3(this.dir).mode & 63) !== 0) chmodSync2(this.dir, 448);
1850
+ }
1851
+ async get(host, slug) {
1852
+ const file = join10(this.dir, entryName(host, slug));
1853
+ if (!existsSync8(this.dir)) return void 0;
1854
+ accessSync(this.dir, constants.R_OK | constants.X_OK);
1855
+ if (!existsSync8(file)) return void 0;
1856
+ return JSON.parse(readFileSync9(file, "utf8"));
1857
+ }
1858
+ async set(host, slug, session) {
1859
+ this.ensureDir();
1860
+ const file = join10(this.dir, entryName(host, slug));
1861
+ const tmp = `${file}.${process.pid}.tmp`;
1862
+ writeFileSync7(tmp, JSON.stringify(session), { mode: 384 });
1863
+ renameSync2(tmp, file);
1864
+ }
1865
+ async clear(host, slug) {
1866
+ if (!existsSync8(this.dir)) return;
1867
+ rmSync6(join10(this.dir, entryName(host, slug)), { force: true });
1868
+ }
1869
+ /** Whether a key could be written here right now, without leaving anything behind. */
1870
+ writable() {
1871
+ try {
1872
+ this.ensureDir();
1873
+ accessSync(this.dir, constants.R_OK | constants.W_OK | constants.X_OK);
1874
+ const probe = join10(this.dir, `.probe.${process.pid}`);
1875
+ writeFileSync7(probe, "", { mode: 384 });
1876
+ rmSync6(probe, { force: true });
1877
+ return { ok: true };
1878
+ } catch (e) {
1879
+ return { ok: false, reason: e.code ?? e.message };
1880
+ }
1881
+ }
1882
+ };
1883
+ function crewifyStoreError(e) {
1884
+ const msg = e instanceof Error ? e.message : String(e);
1885
+ if (!/TABLATION_API_KEY/.test(msg)) return e instanceof Error ? e : new Error(msg);
1886
+ const out = new Error(
1887
+ msg.replace(
1888
+ /Set TABLATION_API_KEY instead of running `login`\./,
1889
+ "Set CREW_API_KEY for the ship key, or set `ship.secretStore: file` in crew.yaml to keep keys under the state directory."
1890
+ ),
1891
+ { cause: e }
1892
+ );
1893
+ return out;
1894
+ }
1895
+ var CrewHintedStore = class {
1896
+ inner;
1897
+ constructor(inner) {
1898
+ this.inner = inner;
1899
+ }
1900
+ async get(host, slug) {
1901
+ try {
1902
+ return await this.inner.get(host, slug);
1903
+ } catch (e) {
1904
+ throw crewifyStoreError(e);
1905
+ }
1906
+ }
1907
+ async set(host, slug, s) {
1908
+ try {
1909
+ await this.inner.set(host, slug, s);
1910
+ } catch (e) {
1911
+ throw crewifyStoreError(e);
1912
+ }
1913
+ }
1914
+ async clear(host, slug) {
1915
+ try {
1916
+ await this.inner.clear(host, slug);
1917
+ } catch (e) {
1918
+ throw crewifyStoreError(e);
1919
+ }
1920
+ }
1921
+ };
1922
+ function osStoreReachable(env = process.env, platform2 = process.platform, hasBinary = onPath) {
1923
+ if (platform2 !== "linux") return true;
1924
+ if (!hasBinary("secret-tool")) return false;
1925
+ if (env.DBUS_SESSION_BUS_ADDRESS) return true;
1926
+ return !!env.XDG_RUNTIME_DIR && existsSync8(join10(env.XDG_RUNTIME_DIR, "bus"));
1927
+ }
1928
+ function onPath(name) {
1929
+ for (const dir of (process.env.PATH ?? "").split(":")) {
1930
+ if (dir && existsSync8(join10(dir, name))) return true;
1931
+ }
1932
+ return false;
1933
+ }
1934
+ function resolveStoreKind(mode, reachable = osStoreReachable()) {
1935
+ if (mode === "file") return "file";
1936
+ if (mode === "os") return "os";
1937
+ return reachable ? "os" : "file";
1938
+ }
1939
+ function describeSecretStore(c = configured) {
1940
+ const mode = c?.mode ?? "auto";
1941
+ const kind = c ? resolveStoreKind(mode) : "os";
1942
+ return kind === "file" && c ? { kind, mode, dir: keysDir(c.stateDir) } : { kind: "os", mode };
1943
+ }
1944
+ function getSessionStore(c = configured) {
1945
+ const r = describeSecretStore(c);
1946
+ if (r.kind === "file" && r.dir) return new FileSessionStore(r.dir);
1947
+ return new CrewHintedStore(getOsSessionStore());
1948
+ }
1949
+ function describeKeyStore(ship) {
1950
+ const c = { mode: ship.secretStore ?? "auto", stateDir: ship.stateDir };
1951
+ const r = describeSecretStore(c);
1952
+ const why = c.mode === "auto" ? " (auto: no desktop secret service reachable)" : ` (ship.secretStore: ${c.mode})`;
1953
+ if (r.kind === "file" && r.dir) {
1954
+ const w = new FileSessionStore(r.dir).writable();
1955
+ return `file ${r.dir}${why} \u2014 ${w.ok ? "writable" : `NOT writable (${w.reason})`}`;
1956
+ }
1957
+ return `OS secret store${c.mode === "auto" ? "" : why}`;
1958
+ }
1822
1959
 
1823
1960
  // src/platform.ts
1824
1961
  var SHIP_PLATFORMS = ["macos", "linux", "windows"];
@@ -1857,8 +1994,8 @@ function explain(ship, required) {
1857
1994
  }
1858
1995
 
1859
1996
  // src/repo-config.ts
1860
- import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
1861
- import { join as join10, basename as basename2 } from "path";
1997
+ import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
1998
+ import { join as join11, basename as basename2 } from "path";
1862
1999
  import { parse } from "yaml";
1863
2000
  var SPEC_VERSION = 1;
1864
2001
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -1942,8 +2079,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
1942
2079
  var PROVIDERS = ["github", "buildkite", "other", "none"];
1943
2080
  function findRepoConfig(dir) {
1944
2081
  for (const name of REPO_CONFIG_FILENAMES) {
1945
- const p = join10(dir, name);
1946
- if (existsSync8(p)) return p;
2082
+ const p = join11(dir, name);
2083
+ if (existsSync9(p)) return p;
1947
2084
  }
1948
2085
  return null;
1949
2086
  }
@@ -2181,7 +2318,7 @@ function parseRepoConfig(text, file) {
2181
2318
  function loadRepoConfig(dir) {
2182
2319
  const file = findRepoConfig(dir);
2183
2320
  if (!file) return null;
2184
- return parseRepoConfig(readFileSync9(file, "utf8"), file);
2321
+ return parseRepoConfig(readFileSync10(file, "utf8"), file);
2185
2322
  }
2186
2323
  function hookLabel(cfg, hook2) {
2187
2324
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -2503,7 +2640,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
2503
2640
  "forges",
2504
2641
  "previewHost",
2505
2642
  "maxPeriscopes",
2506
- "periscopeIdleMinutes"
2643
+ "periscopeIdleMinutes",
2644
+ "secretStore"
2507
2645
  ]);
2508
2646
  var ROUTE_KEYS = /* @__PURE__ */ new Set([
2509
2647
  "route",
@@ -2651,11 +2789,11 @@ function expand(p, base) {
2651
2789
  return isAbsolute(out) ? out : resolve(base, out);
2652
2790
  }
2653
2791
  function resolvePromptsDir(promptSet, crewHome, base) {
2654
- if (!promptSet) return join11(crewHome, "prompts", "default");
2792
+ if (!promptSet) return join12(crewHome, "prompts", "default");
2655
2793
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
2656
2794
  return expand(promptSet, base);
2657
2795
  }
2658
- return join11(crewHome, "prompts", promptSet);
2796
+ return join12(crewHome, "prompts", promptSet);
2659
2797
  }
2660
2798
  var Missing = class {
2661
2799
  paths = [];
@@ -2679,7 +2817,7 @@ var Missing = class {
2679
2817
  };
2680
2818
  function crewVersion(crewHome) {
2681
2819
  try {
2682
- const pkg = JSON.parse(readFileSync10(join11(crewHome, "package.json"), "utf8"));
2820
+ const pkg = JSON.parse(readFileSync11(join12(crewHome, "package.json"), "utf8"));
2683
2821
  return pkg.version ?? "0.0.0";
2684
2822
  } catch {
2685
2823
  return "0.0.0";
@@ -2689,26 +2827,26 @@ function configSearchPath(crewHome) {
2689
2827
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
2690
2828
  const xdg = process.env.XDG_CONFIG_HOME;
2691
2829
  return [
2692
- ...xdg ? [join11(xdg, "crew", "crew.yaml")] : [],
2693
- join11(homedir6(), ".config", "crew", "crew.yaml"),
2830
+ ...xdg ? [join12(xdg, "crew", "crew.yaml")] : [],
2831
+ join12(homedir6(), ".config", "crew", "crew.yaml"),
2694
2832
  resolve(crewHome, "crew.yaml")
2695
2833
  ];
2696
2834
  }
2697
2835
  function findConfigFile(crewHome) {
2698
2836
  const candidates2 = configSearchPath(crewHome);
2699
- return candidates2.find((c) => existsSync9(c)) ?? candidates2[candidates2.length - 1];
2837
+ return candidates2.find((c) => existsSync10(c)) ?? candidates2[candidates2.length - 1];
2700
2838
  }
2701
- var defaultConfigPath = () => join11(process.env.XDG_CONFIG_HOME ?? join11(homedir6(), ".config"), "crew", "crew.yaml");
2839
+ var defaultConfigPath = () => join12(process.env.XDG_CONFIG_HOME ?? join12(homedir6(), ".config"), "crew", "crew.yaml");
2702
2840
  function routeSlug(route2) {
2703
2841
  return route2.replace(/\//g, "-");
2704
2842
  }
2705
2843
  function resolvedPathFor(stateDir, route2) {
2706
2844
  const [workspace, project] = splitRoute(route2);
2707
- return join11(stateDir, "resolved", workspace, `${project}.json`);
2845
+ return join12(stateDir, "resolved", workspace, `${project}.json`);
2708
2846
  }
2709
2847
  function apiKeyPathFor(stateDir, route2) {
2710
2848
  const [workspace, project] = splitRoute(route2);
2711
- return join11(stateDir, "keys", workspace, `${project}.env`);
2849
+ return join12(stateDir, "keys", workspace, `${project}.env`);
2712
2850
  }
2713
2851
  function splitRoute(route2) {
2714
2852
  const parts = route2.split("/");
@@ -2719,9 +2857,9 @@ function splitRoute(route2) {
2719
2857
  }
2720
2858
  function readResolvedFile(stateDir, route2) {
2721
2859
  const path = resolvedPathFor(stateDir, route2);
2722
- if (!existsSync9(path)) return void 0;
2860
+ if (!existsSync10(path)) return void 0;
2723
2861
  try {
2724
- return JSON.parse(readFileSync10(path, "utf8"));
2862
+ return JSON.parse(readFileSync11(path, "utf8"));
2725
2863
  } catch (e) {
2726
2864
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
2727
2865
  }
@@ -2781,18 +2919,18 @@ function parseResolved(raw, m, where) {
2781
2919
  }
2782
2920
  function loadConfig(crewHome, configFile, opts = {}) {
2783
2921
  const file = configFile ?? findConfigFile(crewHome);
2784
- if (opts.text === void 0 && !existsSync9(file)) {
2922
+ if (opts.text === void 0 && !existsSync10(file)) {
2785
2923
  throw new ConfigError(
2786
2924
  `no config found. Looked in:
2787
2925
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
2788
2926
 
2789
2927
  Create the first of those:
2790
- mkdir -p ${dirname4(defaultConfigPath())} && cp ${join11(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2928
+ mkdir -p ${dirname4(defaultConfigPath())} && cp ${join12(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2791
2929
  );
2792
2930
  }
2793
2931
  let raw;
2794
2932
  try {
2795
- raw = parse2(opts.text ?? readFileSync10(file, "utf8"));
2933
+ raw = parse2(opts.text ?? readFileSync11(file, "utf8"));
2796
2934
  } catch (e) {
2797
2935
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
2798
2936
  }
@@ -2846,6 +2984,11 @@ Create the first of those:
2846
2984
  if (!Number.isInteger(periscopeIdleMinutes) || periscopeIdleMinutes < 1) {
2847
2985
  missing.add(`ship.periscopeIdleMinutes must be a positive integer (got "${shipRaw.periscopeIdleMinutes}")`);
2848
2986
  }
2987
+ const secretStoreRaw = shipRaw.secretStore === void 0 ? "auto" : shipRaw.secretStore;
2988
+ if (!isSecretStoreMode(secretStoreRaw)) {
2989
+ missing.add(`ship.secretStore must be one of ${SECRET_STORE_MODES.join(" | ")} (got "${String(shipRaw.secretStore)}")`);
2990
+ }
2991
+ const secretStore = isSecretStoreMode(secretStoreRaw) ? secretStoreRaw : "auto";
2849
2992
  const eventsRotateBytes = shipRaw.eventsRotateBytes === void 0 ? DEFAULT_EVENTS_ROTATE_BYTES : Number(shipRaw.eventsRotateBytes);
2850
2993
  if (!Number.isInteger(eventsRotateBytes) || eventsRotateBytes < 1) {
2851
2994
  missing.add(`ship.eventsRotateBytes must be a positive integer (got "${shipRaw.eventsRotateBytes}")`);
@@ -2885,6 +3028,7 @@ Create the first of those:
2885
3028
  const dupe = seen.find((n, i) => n && seen.indexOf(n) !== i);
2886
3029
  if (dupe) missing.add(`routes: duplicate route "${dupe}" \u2014 a route addresses itself, so it must be unique`);
2887
3030
  missing.throwIfAny(file);
3031
+ configureSecretStore({ mode: secretStore, stateDir });
2888
3032
  return {
2889
3033
  ship: {
2890
3034
  name: shipRaw.name ?? "this ship",
@@ -2902,7 +3046,7 @@ Create the first of those:
2902
3046
  useNvm: shipRaw.useNvm !== false,
2903
3047
  nvmSh: shipRaw.nvmSh,
2904
3048
  stateDir,
2905
- logFile: expand(shipRaw.logFile ?? join11(tmpdir(), "crew.log"), base),
3049
+ logFile: expand(shipRaw.logFile ?? join12(tmpdir(), "crew.log"), base),
2906
3050
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
2907
3051
  maxConcurrentAgents,
2908
3052
  maxConcurrentAgentsSet,
@@ -2919,7 +3063,8 @@ Create the first of those:
2919
3063
  forges: parseShipForges(shipRaw.forges),
2920
3064
  previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
2921
3065
  maxPeriscopes,
2922
- periscopeIdleMinutes
3066
+ periscopeIdleMinutes,
3067
+ secretStore
2923
3068
  },
2924
3069
  routes,
2925
3070
  crewHome,
@@ -3063,7 +3208,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
3063
3208
  const hasRepos = Object.keys(repoDirs).length > 0;
3064
3209
  const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
3065
3210
  const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
3066
- const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join11(reposBasePath, splitRoute(route2)[0]) : "";
3211
+ const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join12(reposBasePath, splitRoute(route2)[0]) : "";
3067
3212
  const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
3068
3213
  if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
3069
3214
  missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
@@ -3160,8 +3305,8 @@ function repoIdForName(r, name) {
3160
3305
  }
3161
3306
  function resolveApiKey(r) {
3162
3307
  if (r.apiKey) return r.apiKey;
3163
- if (r.apiKeyFile && r.apiKeyVar && existsSync9(r.apiKeyFile)) {
3164
- for (const line of readFileSync10(r.apiKeyFile, "utf8").split("\n")) {
3308
+ if (r.apiKeyFile && r.apiKeyVar && existsSync10(r.apiKeyFile)) {
3309
+ for (const line of readFileSync11(r.apiKeyFile, "utf8").split("\n")) {
3165
3310
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
3166
3311
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
3167
3312
  }
@@ -3174,9 +3319,9 @@ function resolveApiKey(r) {
3174
3319
  }
3175
3320
  function credentialSource(r) {
3176
3321
  if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
3177
- if (r.apiKeyFile && r.apiKeyVar && existsSync9(r.apiKeyFile)) {
3322
+ if (r.apiKeyFile && r.apiKeyVar && existsSync10(r.apiKeyFile)) {
3178
3323
  const re = new RegExp(`^${r.apiKeyVar}=`);
3179
- if (readFileSync10(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
3324
+ if (readFileSync11(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
3180
3325
  }
3181
3326
  return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
3182
3327
  }
@@ -3232,7 +3377,7 @@ function configuredMembers(rt) {
3232
3377
  return out;
3233
3378
  }
3234
3379
  function defaultRepoDir(basePath, workspace, repoName) {
3235
- return join11(basePath, workspace, repoName);
3380
+ return join12(basePath, workspace, repoName);
3236
3381
  }
3237
3382
  function reposOf(r) {
3238
3383
  const named = Object.entries(r.repos);
@@ -3265,7 +3410,7 @@ function passengerRepoTargets(r) {
3265
3410
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
3266
3411
  }
3267
3412
  function passengerCheckoutDir(basePath, workspace, repoName) {
3268
- return join11(basePath, workspace, ".passenger-checkouts", repoName);
3413
+ return join12(basePath, workspace, ".passenger-checkouts", repoName);
3269
3414
  }
3270
3415
  function passengerCheckoutTargets(r) {
3271
3416
  if (!r.hostPassengers) return [];
@@ -3372,10 +3517,10 @@ import { platform } from "process";
3372
3517
  function defaultShell() {
3373
3518
  return platform === "win32" ? { bin: "powershell", args: ["-NoProfile", "-NonInteractive", "-Command"] } : { bin: "bash", args: ["-c"] };
3374
3519
  }
3375
- function resolveShell(configured) {
3376
- if (!configured) return defaultShell();
3377
- if (typeof configured !== "string") return configured;
3378
- const bin = configured.trim();
3520
+ function resolveShell(configured2) {
3521
+ if (!configured2) return defaultShell();
3522
+ if (typeof configured2 !== "string") return configured2;
3523
+ const bin = configured2.trim();
3379
3524
  if (/pwsh|powershell/i.test(bin)) {
3380
3525
  return { bin, args: ["-NoProfile", "-NonInteractive", "-Command"] };
3381
3526
  }
@@ -3404,10 +3549,10 @@ function runScript(script, opts) {
3404
3549
  child.stdout.on("data", absorb);
3405
3550
  child.stderr.on("data", absorb);
3406
3551
  const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
3407
- child.on("error", (err2) => {
3552
+ child.on("error", (err3) => {
3408
3553
  if (timer) clearTimeout(timer);
3409
3554
  resolvePromise({ code: 127, output: `${output}
3410
- crew: cannot run ${shell.bin}: ${err2.message}` });
3555
+ crew: cannot run ${shell.bin}: ${err3.message}` });
3411
3556
  });
3412
3557
  child.on("close", (code) => {
3413
3558
  if (timer) clearTimeout(timer);
@@ -3552,11 +3697,11 @@ function recoveredFailureHeadlines(o, route2) {
3552
3697
  }
3553
3698
 
3554
3699
  // src/install.ts
3555
- import { dirname as dirname5, join as join12, extname } from "path";
3700
+ import { dirname as dirname5, join as join13, extname } from "path";
3556
3701
  import { homedir as homedir7 } from "os";
3557
- import { accessSync, constants, existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync3, writeFileSync as writeFileSync7, unlinkSync, realpathSync } from "fs";
3702
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync8, unlinkSync, realpathSync } from "fs";
3558
3703
  import { spawnSync as spawnSync4 } from "child_process";
3559
- import { createHash as createHash2 } from "crypto";
3704
+ import { createHash as createHash3 } from "crypto";
3560
3705
 
3561
3706
  // src/runtime-info.ts
3562
3707
  function isCompiledBinary(moduleUrl) {
@@ -3566,7 +3711,7 @@ function isCompiledBinary(moduleUrl) {
3566
3711
  // src/install.ts
3567
3712
  function crewInvocation(crewHome) {
3568
3713
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
3569
- return [process.execPath, join12(crewHome, "bin", "crew")];
3714
+ return [process.execPath, join13(crewHome, "bin", "crew")];
3570
3715
  }
3571
3716
  var INTERVAL_SECONDS = 120;
3572
3717
  function labelFor(crewHome, job) {
@@ -3577,7 +3722,7 @@ function labelFor(crewHome, job) {
3577
3722
  return crewHome;
3578
3723
  }
3579
3724
  })();
3580
- const hash = createHash2("sha1").update(real).digest("hex").slice(0, 8);
3725
+ const hash = createHash3("sha1").update(real).digest("hex").slice(0, 8);
3581
3726
  const stem = job === "release" ? "com.tablation.crew-release" : job === "passengers" ? "com.tablation.crew-passengers" : "com.tablation.crew";
3582
3727
  return `${stem}.${hash}`;
3583
3728
  }
@@ -3604,17 +3749,17 @@ function pathFor(ship) {
3604
3749
  return parts.join(":");
3605
3750
  }
3606
3751
  function findOnPath(bin, searchPath) {
3607
- const candidates2 = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join12(dir, bin));
3752
+ const candidates2 = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join13(dir, bin));
3608
3753
  for (const candidate of candidates2) {
3609
3754
  try {
3610
- accessSync(candidate, constants.X_OK);
3611
- if (statSync3(candidate).isFile()) return candidate;
3755
+ accessSync2(candidate, constants2.X_OK);
3756
+ if (statSync4(candidate).isFile()) return candidate;
3612
3757
  } catch {
3613
3758
  }
3614
3759
  }
3615
3760
  return void 0;
3616
3761
  }
3617
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join12(homedir7(), ".docker", "bin")].join(":");
3762
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join13(homedir7(), ".docker", "bin")].join(":");
3618
3763
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
3619
3764
  const schedulerPath = pathFor(ship);
3620
3765
  if (findOnPath("docker", schedulerPath)) return void 0;
@@ -3626,7 +3771,7 @@ function xmlEscape(s) {
3626
3771
  }
3627
3772
  function planLaunchd(ship, crewHome, job) {
3628
3773
  const label = labelFor(crewHome, job);
3629
- const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3774
+ const unitPath = join13(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3630
3775
  const crewLog = ship.logFile;
3631
3776
  const schedulerLog = schedulerLogFor(crewLog, job);
3632
3777
  const persistent = job === "run";
@@ -3680,8 +3825,8 @@ ${scheduleKeys}
3680
3825
  }
3681
3826
  function planSystemd(ship, crewHome, job) {
3682
3827
  const label = labelFor(crewHome, job);
3683
- const unitDir = join12(homedir7(), ".config", "systemd", "user");
3684
- const servicePath = join12(unitDir, `${label}.service`);
3828
+ const unitDir = join13(homedir7(), ".config", "systemd", "user");
3829
+ const servicePath = join13(unitDir, `${label}.service`);
3685
3830
  const crewLog = ship.logFile;
3686
3831
  const schedulerLog = schedulerLogFor(crewLog, job);
3687
3832
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -3721,7 +3866,7 @@ WantedBy=default.target
3721
3866
  schedulerLog
3722
3867
  };
3723
3868
  }
3724
- const timerPath = join12(unitDir, `${label}.timer`);
3869
+ const timerPath = join13(unitDir, `${label}.timer`);
3725
3870
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
3726
3871
  [Unit]
3727
3872
  Description=${description}
@@ -3795,7 +3940,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3795
3940
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3796
3941
  const label = labelFor(crewHome, job);
3797
3942
  if (hostShip === "macos") {
3798
- const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3943
+ const unitPath = join13(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3799
3944
  return {
3800
3945
  mechanism: "launchd",
3801
3946
  unitPaths: [unitPath],
@@ -3805,18 +3950,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3805
3950
  };
3806
3951
  }
3807
3952
  if (hostShip === "linux" && hasSystemd) {
3808
- const unitDir = join12(homedir7(), ".config", "systemd", "user");
3953
+ const unitDir = join13(homedir7(), ".config", "systemd", "user");
3809
3954
  if (job === "run") {
3810
3955
  return {
3811
3956
  mechanism: "systemd",
3812
- unitPaths: [join12(unitDir, `${label}.service`)],
3957
+ unitPaths: [join13(unitDir, `${label}.service`)],
3813
3958
  crontabLine: null,
3814
3959
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
3815
3960
  };
3816
3961
  }
3817
3962
  return {
3818
3963
  mechanism: "systemd",
3819
- unitPaths: [join12(unitDir, `${label}.service`), join12(unitDir, `${label}.timer`)],
3964
+ unitPaths: [join13(unitDir, `${label}.service`), join13(unitDir, `${label}.timer`)],
3820
3965
  crontabLine: null,
3821
3966
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
3822
3967
  };
@@ -3834,7 +3979,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3834
3979
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
3835
3980
  const label = labelFor(crewHome, "run");
3836
3981
  if (hostShip === "macos") {
3837
- const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3982
+ const unitPath = join13(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3838
3983
  return {
3839
3984
  mechanism: "launchd",
3840
3985
  startCommand: ["launchctl", "load", unitPath],
@@ -3874,8 +4019,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
3874
4019
  log.emit(`would write ${path}`);
3875
4020
  continue;
3876
4021
  }
3877
- mkdirSync6(dirname5(path), { recursive: true });
3878
- writeFileSync7(path, plan.unitContent[path]);
4022
+ mkdirSync7(dirname5(path), { recursive: true });
4023
+ writeFileSync8(path, plan.unitContent[path]);
3879
4024
  log.emit(`wrote ${path}`);
3880
4025
  }
3881
4026
  if (plan.crontabLine) {
@@ -3929,7 +4074,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3929
4074
  }
3930
4075
  }
3931
4076
  for (const path of plan.unitPaths) {
3932
- if (!existsSync10(path)) continue;
4077
+ if (!existsSync11(path)) continue;
3933
4078
  if (dryRun) {
3934
4079
  log.emit(`would remove ${path}`);
3935
4080
  continue;
@@ -3948,7 +4093,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3948
4093
  }
3949
4094
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
3950
4095
  function launchAgentsDir() {
3951
- return join12(homedir7(), "Library", "LaunchAgents");
4096
+ return join13(homedir7(), "Library", "LaunchAgents");
3952
4097
  }
3953
4098
  function xmlUnescape(s) {
3954
4099
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -3965,25 +4110,25 @@ function loadedCrewLabels() {
3965
4110
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3966
4111
  }
3967
4112
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3968
- const files = existsSync10(dir) ? readdirSync3(dir) : [];
4113
+ const files = existsSync11(dir) ? readdirSync3(dir) : [];
3969
4114
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3970
- const plistPath = join12(dir, f);
4115
+ const plistPath = join13(dir, f);
3971
4116
  let program = null;
3972
4117
  try {
3973
- program = programFromPlist(readFileSync11(plistPath, "utf8"));
4118
+ program = programFromPlist(readFileSync12(plistPath, "utf8"));
3974
4119
  } catch {
3975
4120
  }
3976
4121
  return {
3977
4122
  label: f.slice(0, -".plist".length),
3978
4123
  plistPath,
3979
4124
  program,
3980
- programExists: program !== null && existsSync10(program)
4125
+ programExists: program !== null && existsSync11(program)
3981
4126
  };
3982
4127
  });
3983
4128
  const seen = new Set(units.map((u) => u.label));
3984
4129
  for (const label of [...loaded].sort()) {
3985
4130
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
3986
- units.push({ label, plistPath: join12(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
4131
+ units.push({ label, plistPath: join13(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3987
4132
  }
3988
4133
  return units;
3989
4134
  }
@@ -3996,9 +4141,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3996
4141
  log.emit(`would unload and remove ${unit.label}`);
3997
4142
  return;
3998
4143
  }
3999
- if (existsSync10(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
4144
+ if (existsSync11(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
4000
4145
  spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
4001
- if (existsSync10(unit.plistPath)) unlinkSync(unit.plistPath);
4146
+ if (existsSync11(unit.plistPath)) unlinkSync(unit.plistPath);
4002
4147
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
4003
4148
  }
4004
4149
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -4076,24 +4221,24 @@ function detectSystemd() {
4076
4221
  }
4077
4222
 
4078
4223
  // src/ship-attention.ts
4079
- import { existsSync as existsSync12, statfsSync } from "fs";
4224
+ import { existsSync as existsSync13, statfsSync } from "fs";
4080
4225
 
4081
4226
  // src/git.ts
4082
4227
  import { execFileSync as execFileSync2 } from "child_process";
4083
4228
  import {
4084
- existsSync as existsSync11,
4085
- mkdirSync as mkdirSync7,
4229
+ existsSync as existsSync12,
4230
+ mkdirSync as mkdirSync8,
4086
4231
  openSync as openSync2,
4087
4232
  closeSync as closeSync2,
4088
4233
  writeSync,
4089
- statSync as statSync4,
4234
+ statSync as statSync5,
4090
4235
  unlinkSync as unlinkSync2,
4091
- readFileSync as readFileSync12,
4092
- writeFileSync as writeFileSync8,
4236
+ readFileSync as readFileSync13,
4237
+ writeFileSync as writeFileSync9,
4093
4238
  readdirSync as readdirSync4,
4094
- rmSync as rmSync6
4239
+ rmSync as rmSync7
4095
4240
  } from "fs";
4096
- import { dirname as dirname6, basename as basename4, join as join13 } from "path";
4241
+ import { dirname as dirname6, basename as basename4, join as join14 } from "path";
4097
4242
  var GitError = class extends Error {
4098
4243
  };
4099
4244
  function streamText(x) {
@@ -4103,18 +4248,18 @@ function gitRaw(cwd, args) {
4103
4248
  try {
4104
4249
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4105
4250
  } catch (e) {
4106
- const err2 = e;
4107
- const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4108
- throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
4251
+ const err3 = e;
4252
+ const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
4253
+ throw new GitError(`git ${args.join(" ")}: ${why || err3.message.trim()}`);
4109
4254
  }
4110
4255
  }
4111
4256
  function git(cwd, args) {
4112
4257
  try {
4113
4258
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4114
4259
  } catch (e) {
4115
- const err2 = e;
4116
- const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4117
- throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
4260
+ const err3 = e;
4261
+ const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
4262
+ throw new GitError(`git ${args.join(" ")}: ${why || err3.message.trim()}`);
4118
4263
  }
4119
4264
  }
4120
4265
  function gitOk(cwd, args) {
@@ -4474,7 +4619,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
4474
4619
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
4475
4620
  gitOk(cwd, ["worktree", "remove", "--force", path]);
4476
4621
  const root = dirname6(path);
4477
- if (basename4(root).startsWith("crew-release-")) rmSync6(root, { recursive: true, force: true });
4622
+ if (basename4(root).startsWith("crew-release-")) rmSync7(root, { recursive: true, force: true });
4478
4623
  }
4479
4624
  }
4480
4625
  const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
@@ -4491,12 +4636,12 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
4491
4636
  }
4492
4637
  for (const name of names) {
4493
4638
  if (!name.startsWith("crew-release-")) continue;
4494
- const root = join13(tmp, name);
4639
+ const root = join14(tmp, name);
4495
4640
  try {
4496
- const st = statSync4(root);
4641
+ const st = statSync5(root);
4497
4642
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
4498
- if (existsSync11(join13(root, "checkout"))) continue;
4499
- rmSync6(root, { recursive: true, force: true });
4643
+ if (existsSync12(join14(root, "checkout"))) continue;
4644
+ rmSync7(root, { recursive: true, force: true });
4500
4645
  removed.push(name);
4501
4646
  } catch {
4502
4647
  }
@@ -4545,20 +4690,20 @@ function originUrlOf(dir, remoteName) {
4545
4690
  }
4546
4691
  }
4547
4692
  function ensureRepoCheckout(dir, remote, opts = {}) {
4548
- if (existsSync11(`${dir}/.git`)) return false;
4693
+ if (existsSync12(`${dir}/.git`)) return false;
4549
4694
  if (!remote) {
4550
4695
  throw new GitError(
4551
4696
  `${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`
4552
4697
  );
4553
4698
  }
4554
- mkdirSync7(dirname6(dir), { recursive: true });
4699
+ mkdirSync8(dirname6(dir), { recursive: true });
4555
4700
  const url = cloneUrlFor(remote, opts);
4556
4701
  try {
4557
4702
  execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4558
4703
  } catch (e) {
4559
- const err2 = e;
4560
- const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4561
- throw new GitError(`git clone ${url} ${dir}: ${why || err2.message.trim()}`);
4704
+ const err3 = e;
4705
+ const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
4706
+ throw new GitError(`git clone ${url} ${dir}: ${why || err3.message.trim()}`);
4562
4707
  }
4563
4708
  cloneRecorder?.(dir);
4564
4709
  return true;
@@ -4576,7 +4721,7 @@ function sleepSync(ms3) {
4576
4721
  }
4577
4722
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4578
4723
  const lockPath = checkoutLockPath(dir);
4579
- mkdirSync7(dirname6(lockPath), { recursive: true });
4724
+ mkdirSync8(dirname6(lockPath), { recursive: true });
4580
4725
  const deadline = Date.now() + timeoutMs;
4581
4726
  for (; ; ) {
4582
4727
  try {
@@ -4591,10 +4736,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4591
4736
  }
4592
4737
  };
4593
4738
  } catch (e) {
4594
- const err2 = e;
4595
- if (err2.code !== "EEXIST") throw err2;
4739
+ const err3 = e;
4740
+ if (err3.code !== "EEXIST") throw err3;
4596
4741
  try {
4597
- if (Date.now() - statSync4(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
4742
+ if (Date.now() - statSync5(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
4598
4743
  try {
4599
4744
  unlinkSync2(lockPath);
4600
4745
  } catch {
@@ -4613,17 +4758,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4613
4758
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
4614
4759
  const release = acquireCheckoutLock(dir);
4615
4760
  try {
4616
- if (!existsSync11(`${dir}/.git`) && githubRemote) {
4761
+ if (!existsSync12(`${dir}/.git`) && githubRemote) {
4617
4762
  const url = cloneUrlFor(githubRemote, clone);
4618
4763
  const marker = cloneFailureMarker(dir);
4619
- if (existsSync11(marker) && readFileSync12(marker, "utf8").split("\n")[0] === url) {
4620
- return { action: "clone-failed", detail: readFileSync12(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4764
+ if (existsSync12(marker) && readFileSync13(marker, "utf8").split("\n")[0] === url) {
4765
+ return { action: "clone-failed", detail: readFileSync13(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4621
4766
  }
4622
4767
  try {
4623
4768
  ensureRepoCheckout(dir, githubRemote, clone);
4624
4769
  } catch (e) {
4625
4770
  if (!(e instanceof GitError)) throw e;
4626
- writeFileSync8(marker, `${url}
4771
+ writeFileSync9(marker, `${url}
4627
4772
  ${e.message}`);
4628
4773
  return { action: "clone-failed", detail: e.message, repeat: false };
4629
4774
  }
@@ -4689,6 +4834,15 @@ function baseAttention(repo, outcome, remote, base, nowIso = (/* @__PURE__ */ ne
4689
4834
  if (outcome.action === "level" || outcome.action === "fast-forwarded") return { clear: baseUnsafeKey(repo) };
4690
4835
  return null;
4691
4836
  }
4837
+ var seatKeyUnreadableKey = (route2, seat) => `seat_key_unreadable:${route2}:${seat}`;
4838
+ function seatKeyUnreadableItem(route2, seat, storeError, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
4839
+ return {
4840
+ kind: "seat_key_unreadable",
4841
+ key: seatKeyUnreadableKey(route2, seat),
4842
+ since: nowIso,
4843
+ message: `${route2}: the ${seat} seat's agent key cannot be read (${storeError}) \u2014 its runs are refused until the key store is readable. Resumes automatically once it is; \`crew doctor\` names the store in use`
4844
+ };
4845
+ }
4692
4846
  var agentLogRefusedKey = (route2) => `agent_log_refused:${route2}`;
4693
4847
  function agentLogRefusedItem(route2, role, status2, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
4694
4848
  return {
@@ -4756,7 +4910,7 @@ function repoStopFor(items, route2) {
4756
4910
  };
4757
4911
  }
4758
4912
  function recheckEnvironmentStops(state, route2, ship, deps = {}) {
4759
- const onPath = deps.onPath ?? findOnPath;
4913
+ const onPath2 = deps.onPath ?? findOnPath;
4760
4914
  const dockerProblem = deps.dockerProblem ?? dockerPathProblem;
4761
4915
  const cleared = [];
4762
4916
  for (const item of state.shipAttention().previous()) {
@@ -4779,7 +4933,7 @@ function recheckEnvironmentStops(state, route2, ship, deps = {}) {
4779
4933
  }
4780
4934
  const script = cfg.hooks[h.hook];
4781
4935
  const cmd = script ? hookCommand(script) : null;
4782
- if (!cmd || onPath(cmd, pathFor(ship))) {
4936
+ if (!cmd || onPath2(cmd, pathFor(ship))) {
4783
4937
  clearShipAttention(state, item.key);
4784
4938
  cleared.push(`${h.repo}/${h.hook}`);
4785
4939
  }
@@ -4830,7 +4984,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
4830
4984
  function reconcileHeldWorktrees(state, repo, log, stillRefused) {
4831
4985
  for (const i of state.shipAttention().previous()) {
4832
4986
  if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
4833
- const gone = !existsSync12(i.held.path);
4987
+ const gone = !existsSync13(i.held.path);
4834
4988
  if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
4835
4989
  clearShipAttention(state, i.key);
4836
4990
  log.emit(
@@ -4975,8 +5129,8 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
4975
5129
  run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
4976
5130
  return { state: "ok" };
4977
5131
  } catch (e) {
4978
- const err2 = e;
4979
- const raw = (err2.stderr ? String(err2.stderr) : err2.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
5132
+ const err3 = e;
5133
+ const raw = (err3.stderr ? String(err3.stderr) : err3.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
4980
5134
  const detail = raw || "push dry-run failed";
4981
5135
  const f = o.forge;
4982
5136
  if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
@@ -5081,30 +5235,30 @@ function roleCapWarnings(ship) {
5081
5235
 
5082
5236
  // src/selftest.ts
5083
5237
  import { spawn as spawn3 } from "child_process";
5084
- import { existsSync as existsSync21, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync19, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "fs";
5238
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, mkdtempSync, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync14 } from "fs";
5085
5239
  import { tmpdir as tmpdir2 } from "os";
5086
- import { join as join22 } from "path";
5240
+ import { join as join23 } from "path";
5087
5241
 
5088
5242
  // src/agent.ts
5089
5243
  import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
5090
- import { readFileSync as readFileSync18, existsSync as existsSync20, statSync as statSync6, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5244
+ import { readFileSync as readFileSync19, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5091
5245
  import { StringDecoder } from "string_decoder";
5092
5246
  import { finished } from "stream/promises";
5093
- import { join as join21, dirname as dirname8, basename as basename7 } from "path";
5094
- import { createHash as createHash3 } from "crypto";
5247
+ import { join as join22, dirname as dirname8, basename as basename7 } from "path";
5248
+ import { createHash as createHash4 } from "crypto";
5095
5249
 
5096
5250
  // src/state.ts
5097
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync as rmSync7, statSync as statSync5, readdirSync as readdirSync5 } from "fs";
5098
- import { join as join14 } from "path";
5251
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9, rmSync as rmSync8, statSync as statSync6, readdirSync as readdirSync5 } from "fs";
5252
+ import { join as join15 } from "path";
5099
5253
  var EPOCH = "1970-01-01T00:00:00.000Z";
5100
5254
  var State = class _State {
5101
5255
  dir;
5102
5256
  constructor(dir) {
5103
5257
  this.dir = dir;
5104
- mkdirSync8(dir, { recursive: true });
5258
+ mkdirSync9(dir, { recursive: true });
5105
5259
  }
5106
5260
  path(...parts) {
5107
- return join14(this.dir, ...parts);
5261
+ return join15(this.dir, ...parts);
5108
5262
  }
5109
5263
  /**
5110
5264
  * The timestamp every "new since last poll" check is measured against.
@@ -5115,14 +5269,14 @@ var State = class _State {
5115
5269
  */
5116
5270
  watermark() {
5117
5271
  try {
5118
- const v = readFileSync13(this.path(".poll-watermark"), "utf8").trim();
5272
+ const v = readFileSync14(this.path(".poll-watermark"), "utf8").trim();
5119
5273
  return v || EPOCH;
5120
5274
  } catch {
5121
5275
  return EPOCH;
5122
5276
  }
5123
5277
  }
5124
5278
  setWatermark(at) {
5125
- writeFileSync9(this.path(".poll-watermark"), `${at}
5279
+ writeFileSync10(this.path(".poll-watermark"), `${at}
5126
5280
  `);
5127
5281
  }
5128
5282
  /**
@@ -5140,19 +5294,19 @@ var State = class _State {
5140
5294
  }
5141
5295
  /** Whole-crew pause, and the per-role sentinels one level down. */
5142
5296
  isPaused() {
5143
- return existsSync13(this.path(".crew-paused"));
5297
+ return existsSync14(this.path(".crew-paused"));
5144
5298
  }
5145
5299
  isRolePaused(role) {
5146
- return existsSync13(this.path(`.role-paused-${role}`));
5300
+ return existsSync14(this.path(`.role-paused-${role}`));
5147
5301
  }
5148
5302
  pausedRoles(roles) {
5149
5303
  return new Set(roles.filter((r) => this.isRolePaused(r)));
5150
5304
  }
5151
5305
  pause(role) {
5152
- writeFileSync9(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5306
+ writeFileSync10(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5153
5307
  }
5154
5308
  resume(role) {
5155
- rmSync7(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
5309
+ rmSync8(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
5156
5310
  }
5157
5311
  /**
5158
5312
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -5160,11 +5314,11 @@ var State = class _State {
5160
5314
  * while agents keep working.
5161
5315
  */
5162
5316
  isReleasePaused() {
5163
- return existsSync13(this.path(".release-paused"));
5317
+ return existsSync14(this.path(".release-paused"));
5164
5318
  }
5165
5319
  setReleasePaused(paused) {
5166
- if (paused) writeFileSync9(this.path(".release-paused"), "");
5167
- else rmSync7(this.path(".release-paused"), { force: true });
5320
+ if (paused) writeFileSync10(this.path(".release-paused"), "");
5321
+ else rmSync8(this.path(".release-paused"), { force: true });
5168
5322
  }
5169
5323
  /**
5170
5324
  * Why a release cycle must not run right now, or null when it may. Every
@@ -5201,7 +5355,7 @@ var State = class _State {
5201
5355
  const name = _State.safe(rawName);
5202
5356
  const file = this.path(`.${name}.lock`);
5203
5357
  try {
5204
- const existing = Number.parseInt(readFileSync13(file, "utf8").trim(), 10);
5358
+ const existing = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
5205
5359
  if (Number.isFinite(existing) && existing > 0) {
5206
5360
  try {
5207
5361
  process.kill(existing, 0);
@@ -5211,13 +5365,13 @@ var State = class _State {
5211
5365
  }
5212
5366
  } catch {
5213
5367
  }
5214
- writeFileSync9(file, `${process.pid}
5368
+ writeFileSync10(file, `${process.pid}
5215
5369
  `);
5216
5370
  return {
5217
5371
  ok: true,
5218
5372
  release: () => {
5219
5373
  try {
5220
- rmSync7(file, { force: true });
5374
+ rmSync8(file, { force: true });
5221
5375
  } catch {
5222
5376
  }
5223
5377
  }
@@ -5239,7 +5393,7 @@ var State = class _State {
5239
5393
  if (!/^\.release-.+\.lock$/.test(n)) continue;
5240
5394
  let pid = 0;
5241
5395
  try {
5242
- pid = Number.parseInt(readFileSync13(this.path(n), "utf8").trim(), 10);
5396
+ pid = Number.parseInt(readFileSync14(this.path(n), "utf8").trim(), 10);
5243
5397
  } catch {
5244
5398
  continue;
5245
5399
  }
@@ -5325,7 +5479,7 @@ var State = class _State {
5325
5479
  ticketInFlight(recordId) {
5326
5480
  const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
5327
5481
  try {
5328
- const pid = Number.parseInt(readFileSync13(file, "utf8").trim(), 10);
5482
+ const pid = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
5329
5483
  if (!Number.isFinite(pid) || pid <= 0) return false;
5330
5484
  process.kill(pid, 0);
5331
5485
  return true;
@@ -5359,7 +5513,7 @@ var State = class _State {
5359
5513
  const heldReported = `.release-held-${suffix}`;
5360
5514
  const read = (f) => {
5361
5515
  try {
5362
- return readFileSync13(this.path(f), "utf8").trim() || null;
5516
+ return readFileSync14(this.path(f), "utf8").trim() || null;
5363
5517
  } catch {
5364
5518
  return null;
5365
5519
  }
@@ -5368,12 +5522,12 @@ var State = class _State {
5368
5522
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
5369
5523
  noteBlocked: () => {
5370
5524
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
5371
- writeFileSync9(this.path(blocked), `${n}
5525
+ writeFileSync10(this.path(blocked), `${n}
5372
5526
  `);
5373
5527
  return n;
5374
5528
  },
5375
5529
  clearBlocked: () => {
5376
- rmSync7(this.path(blocked), { force: true });
5530
+ rmSync8(this.path(blocked), { force: true });
5377
5531
  },
5378
5532
  /**
5379
5533
  * The commit a deploy last failed on.
@@ -5385,11 +5539,11 @@ var State = class _State {
5385
5539
  */
5386
5540
  deployFailedSha: () => read(failed),
5387
5541
  noteDeployFailed: (sha) => {
5388
- writeFileSync9(this.path(failed), `${sha}
5542
+ writeFileSync10(this.path(failed), `${sha}
5389
5543
  `);
5390
5544
  },
5391
5545
  clearDeployFailed: () => {
5392
- rmSync7(this.path(failed), { force: true });
5546
+ rmSync8(this.path(failed), { force: true });
5393
5547
  },
5394
5548
  /**
5395
5549
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -5399,31 +5553,31 @@ var State = class _State {
5399
5553
  noteTestGateFailed: (sha) => {
5400
5554
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
5401
5555
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
5402
- writeFileSync9(this.path(gateFailed), `${sha} ${next}
5556
+ writeFileSync10(this.path(gateFailed), `${sha} ${next}
5403
5557
  `);
5404
5558
  return next;
5405
5559
  },
5406
5560
  clearTestGateFailed: () => {
5407
- rmSync7(this.path(gateFailed), { force: true });
5561
+ rmSync8(this.path(gateFailed), { force: true });
5408
5562
  },
5409
5563
  /** Whether the board was already told about this head's red gate. */
5410
5564
  heldReported: () => {
5411
5565
  try {
5412
- return readFileSync13(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5566
+ return readFileSync14(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5413
5567
  } catch {
5414
5568
  return null;
5415
5569
  }
5416
5570
  },
5417
5571
  noteHeldReported: (signature) => {
5418
- writeFileSync9(this.path(heldReported), `${signature}
5572
+ writeFileSync10(this.path(heldReported), `${signature}
5419
5573
  `);
5420
5574
  },
5421
5575
  clearHeldReported: () => {
5422
- rmSync7(this.path(heldReported), { force: true });
5576
+ rmSync8(this.path(heldReported), { force: true });
5423
5577
  },
5424
5578
  testGateReported: (sha) => read(gateReported) === sha,
5425
5579
  noteTestGateReported: (sha) => {
5426
- writeFileSync9(this.path(gateReported), `${sha}
5580
+ writeFileSync10(this.path(gateReported), `${sha}
5427
5581
  `);
5428
5582
  }
5429
5583
  };
@@ -5446,13 +5600,13 @@ var State = class _State {
5446
5600
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
5447
5601
  const read = (f) => {
5448
5602
  try {
5449
- return readFileSync13(this.path(f), "utf8").trim() || null;
5603
+ return readFileSync14(this.path(f), "utf8").trim() || null;
5450
5604
  } catch {
5451
5605
  return null;
5452
5606
  }
5453
5607
  };
5454
5608
  const clear2 = () => {
5455
- for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync7(this.path(f), { force: true });
5609
+ for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync8(this.path(f), { force: true });
5456
5610
  };
5457
5611
  return {
5458
5612
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -5479,18 +5633,18 @@ var State = class _State {
5479
5633
  clear2();
5480
5634
  return;
5481
5635
  }
5482
- writeFileSync9(this.path(streakFile), `${this.readInt(streakFile) + 1}
5636
+ writeFileSync10(this.path(streakFile), `${this.readInt(streakFile) + 1}
5483
5637
  `);
5484
5638
  const prior = read(ticketFile);
5485
5639
  if (topTicket && topTicket === prior) {
5486
- writeFileSync9(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5640
+ writeFileSync10(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5487
5641
  `);
5488
5642
  } else if (topTicket) {
5489
- writeFileSync9(this.path(ticketFile), `${topTicket}
5643
+ writeFileSync10(this.path(ticketFile), `${topTicket}
5490
5644
  `);
5491
- writeFileSync9(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5645
+ writeFileSync10(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5492
5646
  `);
5493
- writeFileSync9(this.path(ticketStreakFile), "1\n");
5647
+ writeFileSync10(this.path(ticketStreakFile), "1\n");
5494
5648
  }
5495
5649
  }
5496
5650
  };
@@ -5511,14 +5665,14 @@ var State = class _State {
5511
5665
  return {
5512
5666
  previous: () => {
5513
5667
  try {
5514
- const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5668
+ const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
5515
5669
  return parsed && typeof parsed === "object" ? parsed : {};
5516
5670
  } catch {
5517
5671
  return {};
5518
5672
  }
5519
5673
  },
5520
5674
  persist: (next) => {
5521
- writeFileSync9(this.path(file), JSON.stringify(next));
5675
+ writeFileSync10(this.path(file), JSON.stringify(next));
5522
5676
  }
5523
5677
  };
5524
5678
  }
@@ -5534,14 +5688,14 @@ var State = class _State {
5534
5688
  return {
5535
5689
  previous: () => {
5536
5690
  try {
5537
- const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5691
+ const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
5538
5692
  return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
5539
5693
  } catch {
5540
5694
  return [];
5541
5695
  }
5542
5696
  },
5543
5697
  persist: (next) => {
5544
- writeFileSync9(this.path(file), JSON.stringify(next));
5698
+ writeFileSync10(this.path(file), JSON.stringify(next));
5545
5699
  }
5546
5700
  };
5547
5701
  }
@@ -5555,14 +5709,14 @@ var State = class _State {
5555
5709
  return {
5556
5710
  previous: () => {
5557
5711
  try {
5558
- const parsed = JSON.parse(readFileSync13(this.path(file), "utf8"));
5712
+ const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
5559
5713
  return Array.isArray(parsed) ? parsed : [];
5560
5714
  } catch {
5561
5715
  return [];
5562
5716
  }
5563
5717
  },
5564
5718
  persist: (next) => {
5565
- writeFileSync9(this.path(file), JSON.stringify(next));
5719
+ writeFileSync10(this.path(file), JSON.stringify(next));
5566
5720
  }
5567
5721
  };
5568
5722
  }
@@ -5575,11 +5729,11 @@ var State = class _State {
5575
5729
  const file = ".release-last-run";
5576
5730
  return {
5577
5731
  touch: () => {
5578
- writeFileSync9(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5732
+ writeFileSync10(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5579
5733
  },
5580
5734
  at: () => {
5581
5735
  try {
5582
- return statSync5(this.path(file)).mtimeMs;
5736
+ return statSync6(this.path(file)).mtimeMs;
5583
5737
  } catch {
5584
5738
  return null;
5585
5739
  }
@@ -5588,7 +5742,7 @@ var State = class _State {
5588
5742
  }
5589
5743
  readInt(file) {
5590
5744
  try {
5591
- return Number.parseInt(readFileSync13(this.path(file), "utf8").trim(), 10) || 0;
5745
+ return Number.parseInt(readFileSync14(this.path(file), "utf8").trim(), 10) || 0;
5592
5746
  } catch {
5593
5747
  return 0;
5594
5748
  }
@@ -6071,8 +6225,8 @@ var PERISCOPE_COLUMNS = [
6071
6225
  ];
6072
6226
 
6073
6227
  // src/agent-log.ts
6074
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "fs";
6075
- import { join as join15 } from "path";
6228
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "fs";
6229
+ import { join as join16 } from "path";
6076
6230
 
6077
6231
  // src/pending-backoff.ts
6078
6232
  var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
@@ -6244,20 +6398,20 @@ async function addRunToTicket(target, t, recordId, run2) {
6244
6398
  };
6245
6399
  }
6246
6400
  }
6247
- var queueDir = (stateDir) => join15(stateDir, "pending-agent-logs");
6401
+ var queueDir = (stateDir) => join16(stateDir, "pending-agent-logs");
6248
6402
  function queueAgentRun(stateDir, name, q) {
6249
- mkdirSync9(queueDir(stateDir), { recursive: true });
6250
- writeFileSync10(join15(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
6403
+ mkdirSync10(queueDir(stateDir), { recursive: true });
6404
+ writeFileSync11(join16(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
6251
6405
  }
6252
6406
  async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6253
6407
  const dir = queueDir(stateDir);
6254
- if (!existsSync14(dir)) return 0;
6408
+ if (!existsSync15(dir)) return 0;
6255
6409
  let sent = 0;
6256
6410
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
6257
- const path = join15(dir, f);
6411
+ const path = join16(dir, f);
6258
6412
  let q;
6259
6413
  try {
6260
- q = JSON.parse(readFileSync14(path, "utf8"));
6414
+ q = JSON.parse(readFileSync15(path, "utf8"));
6261
6415
  } catch {
6262
6416
  continue;
6263
6417
  }
@@ -6276,17 +6430,17 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
6276
6430
  attempts: 0,
6277
6431
  nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
6278
6432
  });
6279
- rmSync8(path, { force: true });
6433
+ rmSync9(path, { force: true });
6280
6434
  sent += 1;
6281
6435
  log.emit(`sent a queued Agent Log report (${f})`);
6282
6436
  } catch (e) {
6283
6437
  if (!(e instanceof AgentLogUnreachableError)) {
6284
- rmSync8(path, { force: true });
6438
+ rmSync9(path, { force: true });
6285
6439
  log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
6286
6440
  continue;
6287
6441
  }
6288
6442
  const attempts = q.attempts + 1;
6289
- writeFileSync10(path, JSON.stringify({
6443
+ writeFileSync11(path, JSON.stringify({
6290
6444
  ...q,
6291
6445
  attempts,
6292
6446
  warned: true,
@@ -6297,20 +6451,20 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
6297
6451
  }
6298
6452
  return sent;
6299
6453
  }
6300
- var countersDir = (stateDir) => join15(stateDir, "pending-ticket-counters");
6454
+ var countersDir = (stateDir) => join16(stateDir, "pending-ticket-counters");
6301
6455
  function queueTicketCounters(stateDir, name, q) {
6302
- mkdirSync9(countersDir(stateDir), { recursive: true });
6303
- writeFileSync10(join15(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
6456
+ mkdirSync10(countersDir(stateDir), { recursive: true });
6457
+ writeFileSync11(join16(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
6304
6458
  }
6305
6459
  async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6306
6460
  const dir = countersDir(stateDir);
6307
- if (!existsSync14(dir)) return 0;
6461
+ if (!existsSync15(dir)) return 0;
6308
6462
  let sent = 0;
6309
6463
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
6310
- const path = join15(dir, f);
6464
+ const path = join16(dir, f);
6311
6465
  let q;
6312
6466
  try {
6313
- q = JSON.parse(readFileSync14(path, "utf8"));
6467
+ q = JSON.parse(readFileSync15(path, "utf8"));
6314
6468
  } catch {
6315
6469
  continue;
6316
6470
  }
@@ -6319,17 +6473,17 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
6319
6473
  const apiKey = await keyFor(q.role);
6320
6474
  if (!apiKey) continue;
6321
6475
  await writeCounters({ ...q.target, apiKey }, q.write);
6322
- rmSync8(path, { force: true });
6476
+ rmSync9(path, { force: true });
6323
6477
  sent += 1;
6324
6478
  log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
6325
6479
  } catch (e) {
6326
6480
  if (!isRetryable(e)) {
6327
- rmSync8(path, { force: true });
6481
+ rmSync9(path, { force: true });
6328
6482
  log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
6329
6483
  continue;
6330
6484
  }
6331
6485
  const attempts = q.attempts + 1;
6332
- writeFileSync10(path, JSON.stringify({
6486
+ writeFileSync11(path, JSON.stringify({
6333
6487
  ...q,
6334
6488
  attempts,
6335
6489
  warned: true,
@@ -6532,8 +6686,8 @@ var HandoffTranscriptTracker = class {
6532
6686
  };
6533
6687
 
6534
6688
  // src/pending-handoff.ts
6535
- import { existsSync as existsSync17, mkdirSync as mkdirSync10, readdirSync as readdirSync7, readFileSync as readFileSync15, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "fs";
6536
- import { join as join18 } from "path";
6689
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync16, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
6690
+ import { join as join19 } from "path";
6537
6691
 
6538
6692
  // src/ticket-branch.ts
6539
6693
  import { basename as basename6 } from "path";
@@ -7205,8 +7359,8 @@ var Tracker = class {
7205
7359
  if (res.status === 404) return [];
7206
7360
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
7207
7361
  rows = await res.json();
7208
- } catch (err2) {
7209
- this.onReadFailure?.(`reading ${type} events failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
7362
+ } catch (err3) {
7363
+ this.onReadFailure?.(`reading ${type} events failed: ${err3 instanceof Error ? err3.message : String(err3)}`);
7210
7364
  return [];
7211
7365
  }
7212
7366
  return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
@@ -7616,13 +7770,13 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
7616
7770
  }
7617
7771
 
7618
7772
  // src/run-worktree.ts
7619
- import { existsSync as existsSync16 } from "fs";
7620
- import { dirname as dirname7, join as join17 } from "path";
7773
+ import { existsSync as existsSync17 } from "fs";
7774
+ import { dirname as dirname7, join as join18 } from "path";
7621
7775
 
7622
7776
  // src/ports.ts
7623
7777
  import { execFileSync as execFileSync4 } from "child_process";
7624
- import { existsSync as existsSync15 } from "fs";
7625
- import { join as join16 } from "path";
7778
+ import { existsSync as existsSync16 } from "fs";
7779
+ import { join as join17 } from "path";
7626
7780
  var run = (cmd, args) => {
7627
7781
  try {
7628
7782
  return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7667,7 +7821,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7667
7821
  }
7668
7822
  function worktreeExists(parent, prefix, n) {
7669
7823
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7670
- if (existsSync15(join16(parent, name, ".git"))) return true;
7824
+ if (existsSync16(join17(parent, name, ".git"))) return true;
7671
7825
  }
7672
7826
  return false;
7673
7827
  }
@@ -7750,7 +7904,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7750
7904
  const existing = worktreeForNumber(dir, num);
7751
7905
  const located = locateBranchForTicket(dir, cfg, t, role);
7752
7906
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7753
- if (existing && !existing.prunable && existsSync16(existing.path)) {
7907
+ if (existing && !existing.prunable && existsSync17(existing.path)) {
7754
7908
  const branch = existing.branch;
7755
7909
  if (status(existing.path).length > 0) {
7756
7910
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -7766,7 +7920,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7766
7920
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7767
7921
  return { action: "recut", path: existing.path, branch, base: baseRef };
7768
7922
  }
7769
- const path = join17(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7923
+ const path = join18(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
7770
7924
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7771
7925
  return { action: "cut-remote", path, branch: located.branch };
7772
7926
  }
@@ -7820,7 +7974,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7820
7974
  if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
7821
7975
  if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
7822
7976
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7823
- if (wt && existsSync16(wt.path)) {
7977
+ if (wt && existsSync17(wt.path)) {
7824
7978
  if (status(wt.path).length > 0) return false;
7825
7979
  if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
7826
7980
  }
@@ -7828,7 +7982,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7828
7982
  }
7829
7983
  async function finishRunWorktree(dir, cfg, branch) {
7830
7984
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7831
- if (!wt || !existsSync16(wt.path)) return { kind: "none" };
7985
+ if (!wt || !existsSync17(wt.path)) return { kind: "none" };
7832
7986
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7833
7987
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7834
7988
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7840,7 +7994,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7840
7994
  if (pids.length) await killGently(pids);
7841
7995
  }
7842
7996
  gitOk(dir, ["worktree", "remove", wt.path]);
7843
- return existsSync16(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7997
+ return existsSync17(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7844
7998
  }
7845
7999
 
7846
8000
  // src/handoff-guard.ts
@@ -8223,20 +8377,20 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
8223
8377
 
8224
8378
  // src/pending-handoff.ts
8225
8379
  var PENDING_MAX_AGE_MS = 48 * 36e5;
8226
- var pendingDirFor = (stateDir) => join18(stateDir, "pending-handoffs");
8227
- var stateDirOfStream = (streamPath) => join18(streamPath, "..", "..");
8380
+ var pendingDirFor = (stateDir) => join19(stateDir, "pending-handoffs");
8381
+ var stateDirOfStream = (streamPath) => join19(streamPath, "..", "..");
8228
8382
  function writePendingHandoff(stateDir, rec) {
8229
8383
  const dir = pendingDirFor(stateDir);
8230
- mkdirSync10(dir, { recursive: true });
8231
- writeFileSync11(join18(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
8384
+ mkdirSync11(dir, { recursive: true });
8385
+ writeFileSync12(join19(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
8232
8386
  }
8233
8387
  function listPendingHandoffs(stateDir, route2) {
8234
8388
  const dir = pendingDirFor(stateDir);
8235
- if (!existsSync17(dir)) return [];
8389
+ if (!existsSync18(dir)) return [];
8236
8390
  const out = [];
8237
8391
  for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
8238
8392
  try {
8239
- const rec = JSON.parse(readFileSync15(join18(dir, f), "utf8"));
8393
+ const rec = JSON.parse(readFileSync16(join19(dir, f), "utf8"));
8240
8394
  if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
8241
8395
  } catch {
8242
8396
  }
@@ -8244,7 +8398,7 @@ function listPendingHandoffs(stateDir, route2) {
8244
8398
  return out;
8245
8399
  }
8246
8400
  function removePendingHandoff(stateDir, ticketRecordId) {
8247
- rmSync9(join18(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
8401
+ rmSync10(join19(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
8248
8402
  }
8249
8403
  function pendingHandoffFor(stateDir, ticketRecordId) {
8250
8404
  return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
@@ -8354,8 +8508,8 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
8354
8508
  }
8355
8509
 
8356
8510
  // src/skills.ts
8357
- import { readFileSync as readFileSync17, readdirSync as readdirSync8, existsSync as existsSync19 } from "fs";
8358
- import { join as join20 } from "path";
8511
+ import { readFileSync as readFileSync18, readdirSync as readdirSync8, existsSync as existsSync20 } from "fs";
8512
+ import { join as join21 } from "path";
8359
8513
  import { parse as parseYaml } from "yaml";
8360
8514
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
8361
8515
 
@@ -8371,8 +8525,8 @@ ${additions}`;
8371
8525
  }
8372
8526
 
8373
8527
  // src/agents.ts
8374
- import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
8375
- import { join as join19 } from "path";
8528
+ import { readFileSync as readFileSync17, existsSync as existsSync19 } from "fs";
8529
+ import { join as join20 } from "path";
8376
8530
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
8377
8531
  var PERSONA_NAME = {
8378
8532
  dev: "Developer",
@@ -8386,12 +8540,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8386
8540
  var AgentsSyncError = class extends Error {
8387
8541
  };
8388
8542
  function personaDefaultPrompt(promptsDir, role) {
8389
- const briefPath = join19(promptsDir, "personas", `lane-${role}.md`);
8390
- if (!existsSync18(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8391
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync16(briefPath, "utf8");
8392
- const commonPath = join19(promptsDir, "personas", "common.md");
8393
- if (!existsSync18(commonPath)) return readFileSync16(briefPath, "utf8");
8394
- return `${readFileSync16(commonPath, "utf8")}${readFileSync16(briefPath, "utf8")}`;
8543
+ const briefPath = join20(promptsDir, "personas", `lane-${role}.md`);
8544
+ if (!existsSync19(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8545
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync17(briefPath, "utf8");
8546
+ const commonPath = join20(promptsDir, "personas", "common.md");
8547
+ if (!existsSync19(commonPath)) return readFileSync17(briefPath, "utf8");
8548
+ return `${readFileSync17(commonPath, "utf8")}${readFileSync17(briefPath, "utf8")}`;
8395
8549
  }
8396
8550
  async function resolveAgentId(route2, role, opts) {
8397
8551
  const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
@@ -8838,7 +8992,7 @@ function describeCrewLink(o) {
8838
8992
  var SkillSyncError = class extends Error {
8839
8993
  };
8840
8994
  function parseSkillFile(path) {
8841
- const text = readFileSync17(path, "utf8");
8995
+ const text = readFileSync18(path, "utf8");
8842
8996
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
8843
8997
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
8844
8998
  const frontmatter = match[1];
@@ -8851,9 +9005,9 @@ function parseSkillFile(path) {
8851
9005
  return { name: meta.name, description: meta.description, prompt: body, file: path };
8852
9006
  }
8853
9007
  function listLocalSkills(promptsDir) {
8854
- const dir = join20(promptsDir, "skills");
8855
- if (!existsSync19(dir)) return [];
8856
- return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join20(dir, f)));
9008
+ const dir = join21(promptsDir, "skills");
9009
+ if (!existsSync20(dir)) return [];
9010
+ return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join21(dir, f)));
8857
9011
  }
8858
9012
  async function syncSkills(route2, opts) {
8859
9013
  if (!route2.resolved) {
@@ -9055,26 +9209,26 @@ var AgentError = class extends Error {
9055
9209
  };
9056
9210
  var AgentSpawnError = class extends AgentError {
9057
9211
  };
9058
- var digestPathFor = (stateDir, routeLabel, role, runId) => join21(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9212
+ var digestPathFor = (stateDir, routeLabel, role, runId) => join22(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9059
9213
  function planAgentRun(o) {
9060
- const briefPath = join21(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9061
- if (!existsSync20(briefPath)) {
9214
+ const briefPath = join22(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9215
+ if (!existsSync21(briefPath)) {
9062
9216
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
9063
9217
  }
9064
- const commonPath = join21(o.route.promptsDir, "personas", "common.md");
9065
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync20(commonPath);
9218
+ const commonPath = join22(o.route.promptsDir, "personas", "common.md");
9219
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync21(commonPath);
9066
9220
  const now = o.now ?? (() => Date.now());
9067
9221
  const routeLabel = routeSlug(o.route.route);
9068
9222
  const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
9069
- const digestPath2 = ownDigest && existsSync20(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
9223
+ const digestPath2 = ownDigest && existsSync21(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
9070
9224
  let digest;
9071
9225
  let ageSeconds;
9072
- if (existsSync20(digestPath2)) {
9073
- ageSeconds = Math.floor((now() - statSync6(digestPath2).mtimeMs) / 1e3);
9074
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync18(digestPath2, "utf8");
9226
+ if (existsSync21(digestPath2)) {
9227
+ ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
9228
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync19(digestPath2, "utf8");
9075
9229
  }
9076
- const common = usesCommon ? readFileSync18(commonPath, "utf8") : "";
9077
- const brief = readFileSync18(briefPath, "utf8");
9230
+ const common = usesCommon ? readFileSync19(commonPath, "utf8") : "";
9231
+ const brief = readFileSync19(briefPath, "utf8");
9078
9232
  const effectiveCommon = o.divergedPrompt ? "" : common;
9079
9233
  const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
9080
9234
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -9095,8 +9249,8 @@ function planAgentRun(o) {
9095
9249
  brief: expanded.text,
9096
9250
  digest
9097
9251
  });
9098
- const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
9099
- const streamsDir = join21(o.stateDir, "streams");
9252
+ const promptSha = createHash4("sha256").update(expanded.text).digest("hex");
9253
+ const streamsDir = join22(o.stateDir, "streams");
9100
9254
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
9101
9255
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
9102
9256
  const { toolchain, model } = seat;
@@ -9169,8 +9323,8 @@ function planAgentRun(o) {
9169
9323
  digestAttached: digest !== void 0,
9170
9324
  digestAgeSeconds: ageSeconds,
9171
9325
  model,
9172
- streamPath: join21(streamsDir, `${base}.jsonl`),
9173
- eventsPath: join21(streamsDir, `${base}.events.jsonl`),
9326
+ streamPath: join22(streamsDir, `${base}.jsonl`),
9327
+ eventsPath: join22(streamsDir, `${base}.events.jsonl`),
9174
9328
  ticket: o.ticket,
9175
9329
  ticketRecordId: o.ticketRecordId ?? null,
9176
9330
  repo: o.repo ?? null,
@@ -9201,7 +9355,7 @@ function describePlan(p) {
9201
9355
  var pidPathFor = (streamPath) => `${streamPath}.pid`;
9202
9356
  function readRunSidecar(streamPath) {
9203
9357
  try {
9204
- const sc = JSON.parse(readFileSync18(pidPathFor(streamPath), "utf8"));
9358
+ const sc = JSON.parse(readFileSync19(pidPathFor(streamPath), "utf8"));
9205
9359
  return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
9206
9360
  } catch {
9207
9361
  return null;
@@ -9237,7 +9391,7 @@ function identityOf(plan, adapterHarness) {
9237
9391
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
9238
9392
  function openSink(path) {
9239
9393
  try {
9240
- mkdirSync11(dirname8(path), { recursive: true });
9394
+ mkdirSync12(dirname8(path), { recursive: true });
9241
9395
  return createWriteStream(path, { flags: "a" });
9242
9396
  } catch {
9243
9397
  return void 0;
@@ -9419,7 +9573,7 @@ var RunObserver = class {
9419
9573
  };
9420
9574
  function writeRunMeta(id, startedAt, liveTicket, extra) {
9421
9575
  try {
9422
- writeFileSync12(`${id.streamPath}.meta.json`, JSON.stringify({
9576
+ writeFileSync13(`${id.streamPath}.meta.json`, JSON.stringify({
9423
9577
  route: id.route,
9424
9578
  role: id.role,
9425
9579
  hint: id.ticket,
@@ -9579,7 +9733,7 @@ function spawnAgent(plan, emit, hintMoved) {
9579
9733
  return new Promise((resolve6, reject) => {
9580
9734
  let streamFd;
9581
9735
  try {
9582
- mkdirSync11(dirname8(plan.streamPath), { recursive: true });
9736
+ mkdirSync12(dirname8(plan.streamPath), { recursive: true });
9583
9737
  streamFd = openSync3(plan.streamPath, "a");
9584
9738
  } catch (e) {
9585
9739
  void endSink(eventsSink);
@@ -9614,7 +9768,7 @@ function spawnAgent(plan, emit, hintMoved) {
9614
9768
  promptSha: plan.promptSha
9615
9769
  };
9616
9770
  try {
9617
- writeFileSync12(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9771
+ writeFileSync13(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9618
9772
  } catch {
9619
9773
  }
9620
9774
  }
@@ -9625,10 +9779,10 @@ function spawnAgent(plan, emit, hintMoved) {
9625
9779
  const tail3 = new FileTail(plan.streamPath);
9626
9780
  const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
9627
9781
  let spawnFailed = false;
9628
- child.on("error", (err2) => {
9782
+ child.on("error", (err3) => {
9629
9783
  spawnFailed = true;
9630
9784
  clearInterval(timer);
9631
- reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err2.message}`));
9785
+ reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
9632
9786
  });
9633
9787
  child.on("close", async (code) => {
9634
9788
  clearInterval(timer);
@@ -9639,8 +9793,8 @@ function spawnAgent(plan, emit, hintMoved) {
9639
9793
  const { ms: ms3, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
9640
9794
  resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {} });
9641
9795
  });
9642
- child.stdin?.on("error", (err2) => {
9643
- emit.warn(`agent closed stdin before the prompt was delivered: ${err2.message}`, { step: "agent" });
9796
+ child.stdin?.on("error", (err3) => {
9797
+ emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
9644
9798
  });
9645
9799
  child.stdin?.end(plan.prompt);
9646
9800
  });
@@ -9680,7 +9834,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
9680
9834
  }
9681
9835
 
9682
9836
  // src/seat-keys.ts
9683
- import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
9837
+ import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
9684
9838
  var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
9685
9839
  function keyIsAdmin(route2) {
9686
9840
  const role = route2.resolved?.role;
@@ -9699,7 +9853,7 @@ function slugFor(route2, agentId) {
9699
9853
  function seatAgentId(route2, role) {
9700
9854
  return route2.resolved?.agentPersonas?.[role]?.agentId;
9701
9855
  }
9702
- async function resolveReleaseKey(route2, getStore = getSessionStore2) {
9856
+ async function resolveReleaseKey(route2, getStore = getSessionStore) {
9703
9857
  const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
9704
9858
  const agentId = route2.resolved?.releaseAgentId;
9705
9859
  if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
@@ -9708,10 +9862,11 @@ async function resolveReleaseKey(route2, getStore = getSessionStore2) {
9708
9862
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
9709
9863
  return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
9710
9864
  } catch (e) {
9711
- return ship(describeReadError(e));
9865
+ const reason = describeReadError(e);
9866
+ return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
9712
9867
  }
9713
9868
  }
9714
- async function resolveSeatKey(route2, role, getStore = getSessionStore2) {
9869
+ async function resolveSeatKey(route2, role, getStore = getSessionStore) {
9715
9870
  const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
9716
9871
  if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
9717
9872
  const agentId = seatAgentId(route2, role);
@@ -9721,12 +9876,13 @@ async function resolveSeatKey(route2, role, getStore = getSessionStore2) {
9721
9876
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
9722
9877
  return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
9723
9878
  } catch (e) {
9724
- return ship(describeReadError(e));
9879
+ const reason = describeReadError(e);
9880
+ return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
9725
9881
  }
9726
9882
  }
9727
9883
  function safeStderrLine(e) {
9728
- const err2 = e;
9729
- const raw = typeof err2?.stderr === "string" && err2.stderr.trim() ? err2.stderr : typeof err2?.message === "string" ? err2.message : "";
9884
+ const err3 = e;
9885
+ const raw = typeof err3?.stderr === "string" && err3.stderr.trim() ? err3.stderr : typeof err3?.message === "string" ? err3.message : "";
9730
9886
  for (const line of raw.split("\n")) {
9731
9887
  const t = line.trim();
9732
9888
  if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
@@ -9735,6 +9891,8 @@ function safeStderrLine(e) {
9735
9891
  return void 0;
9736
9892
  }
9737
9893
  function describeReadError(e) {
9894
+ const fsErr = e;
9895
+ if (typeof fsErr?.path === "string" && typeof fsErr.code === "string") return `key file store unreadable (${fsErr.code})`;
9738
9896
  const code = e?.code;
9739
9897
  const line = safeStderrLine(e);
9740
9898
  const what = typeof code === "number" ? `security exited ${code}` : typeof code === "string" ? `security failed: ${code}` : void 0;
@@ -9763,7 +9921,7 @@ async function mintMissingSeatKeys(route2, o) {
9763
9921
  ];
9764
9922
  let store;
9765
9923
  try {
9766
- store = (o.getStore ?? getSessionStore2)();
9924
+ store = (o.getStore ?? getSessionStore)();
9767
9925
  } catch (e) {
9768
9926
  return targets.map(({ role }) => ({
9769
9927
  role,
@@ -9886,7 +10044,7 @@ async function revokeSeatKeys(route2, o) {
9886
10044
  ];
9887
10045
  let store;
9888
10046
  try {
9889
- store = (o.getStore ?? getSessionStore2)();
10047
+ store = (o.getStore ?? getSessionStore)();
9890
10048
  } catch (e) {
9891
10049
  return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
9892
10050
  }
@@ -10117,7 +10275,7 @@ async function runSelftest(i, deps = realDeps) {
10117
10275
  ...adapter.runEnv(spec),
10118
10276
  [KEY_VAR]: i.apiKey
10119
10277
  };
10120
- const cwd = mkdtempSync(join22(tmpdir2(), "crew-selftest-"));
10278
+ const cwd = mkdtempSync(join23(tmpdir2(), "crew-selftest-"));
10121
10279
  try {
10122
10280
  const run2 = await deps.run({
10123
10281
  bin: invocation.bin,
@@ -10139,7 +10297,7 @@ async function runSelftest(i, deps = realDeps) {
10139
10297
  return finish({ ok: false, failure, detail, ...usage2 });
10140
10298
  } finally {
10141
10299
  try {
10142
- rmSync10(cwd, { recursive: true, force: true });
10300
+ rmSync11(cwd, { recursive: true, force: true });
10143
10301
  } catch {
10144
10302
  }
10145
10303
  }
@@ -10150,11 +10308,11 @@ function formatResult(r) {
10150
10308
  const secs = `${(r.ms / 1e3).toFixed(1)}s`;
10151
10309
  return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
10152
10310
  }
10153
- var fileFor2 = (stateDir) => join22(stateDir, "selftest.json");
10311
+ var fileFor2 = (stateDir) => join23(stateDir, "selftest.json");
10154
10312
  function readSelftest(stateDir) {
10155
10313
  try {
10156
10314
  const p = fileFor2(stateDir);
10157
- return existsSync21(p) ? JSON.parse(readFileSync19(p, "utf8")) : {};
10315
+ return existsSync22(p) ? JSON.parse(readFileSync20(p, "utf8")) : {};
10158
10316
  } catch {
10159
10317
  return {};
10160
10318
  }
@@ -10162,8 +10320,8 @@ function readSelftest(stateDir) {
10162
10320
  function recordSelftest(stateDir, results) {
10163
10321
  const all = readSelftest(stateDir);
10164
10322
  for (const r of results) all[r.toolchain] = r;
10165
- mkdirSync12(stateDir, { recursive: true });
10166
- writeFileSync13(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10323
+ mkdirSync13(stateDir, { recursive: true });
10324
+ writeFileSync14(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10167
10325
  `);
10168
10326
  }
10169
10327
  function doctorSelftestLines(stored, toolchains) {
@@ -10175,7 +10333,7 @@ function doctorSelftestLines(stored, toolchains) {
10175
10333
  }
10176
10334
 
10177
10335
  // src/cli.ts
10178
- import { resolve as resolve5, dirname as dirname22, join as join43, basename as basename13 } from "path";
10336
+ import { resolve as resolve5, dirname as dirname22, join as join44, basename as basename13 } from "path";
10179
10337
  import { fileURLToPath } from "url";
10180
10338
  import { hostname as hostname2, homedir as homedir10 } from "os";
10181
10339
  import { spawnSync as spawnSync7 } from "child_process";
@@ -10220,6 +10378,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
10220
10378
  "service",
10221
10379
  "topic",
10222
10380
  "assignee",
10381
+ "requested-by",
10223
10382
  "issue",
10224
10383
  "shell",
10225
10384
  "resume"
@@ -10296,10 +10455,10 @@ function seats(roster) {
10296
10455
  function holds(roster) {
10297
10456
  return [...roster.values()].filter((m) => m.kind === "hold");
10298
10457
  }
10299
- function buildRoster(configured, crewRows) {
10458
+ function buildRoster(configured2, crewRows) {
10300
10459
  const rows = new Map(crewRows.map((r) => [r.id, r]));
10301
10460
  const roster = /* @__PURE__ */ new Map();
10302
- for (const c of configured) {
10461
+ for (const c of configured2) {
10303
10462
  if (!c.id) continue;
10304
10463
  const row = rows.get(c.id);
10305
10464
  const name = (row?.name ?? "").trim();
@@ -10365,14 +10524,14 @@ function rosterMarkdown(roster, meId) {
10365
10524
  }
10366
10525
 
10367
10526
  // src/connect-resume.ts
10368
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync14 } from "fs";
10369
- import { dirname as dirname9, join as join23 } from "path";
10527
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "fs";
10528
+ import { dirname as dirname9, join as join24 } from "path";
10370
10529
  function lastSessionFile(stateDir) {
10371
- return join23(stateDir, "connect-session.json");
10530
+ return join24(stateDir, "connect-session.json");
10372
10531
  }
10373
10532
  function readPointers(stateDir) {
10374
10533
  try {
10375
- const parsed = JSON.parse(readFileSync20(lastSessionFile(stateDir), "utf8"));
10534
+ const parsed = JSON.parse(readFileSync21(lastSessionFile(stateDir), "utf8"));
10376
10535
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
10377
10536
  const out = {};
10378
10537
  for (const [host, v] of Object.entries(parsed)) {
@@ -10386,8 +10545,8 @@ function readPointers(stateDir) {
10386
10545
  }
10387
10546
  function writePointers(stateDir, pointers) {
10388
10547
  const file = lastSessionFile(stateDir);
10389
- mkdirSync13(dirname9(file), { recursive: true });
10390
- writeFileSync14(file, `${JSON.stringify(pointers, null, 2)}
10548
+ mkdirSync14(dirname9(file), { recursive: true });
10549
+ writeFileSync15(file, `${JSON.stringify(pointers, null, 2)}
10391
10550
  `, { mode: 384 });
10392
10551
  }
10393
10552
  function rememberSession(stateDir, host, workspaceSlug) {
@@ -10404,7 +10563,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
10404
10563
  const { [host]: _old, ...rest } = all;
10405
10564
  const next = left.length ? { ...rest, [host]: left } : rest;
10406
10565
  if (Object.keys(next).length) writePointers(stateDir, next);
10407
- else rmSync11(lastSessionFile(stateDir), { force: true });
10566
+ else rmSync12(lastSessionFile(stateDir), { force: true });
10408
10567
  } catch {
10409
10568
  }
10410
10569
  }
@@ -10444,7 +10603,7 @@ function forgetSession(stateDir, host) {
10444
10603
  try {
10445
10604
  const { [host]: _gone, ...rest } = readPointers(stateDir);
10446
10605
  if (Object.keys(rest).length) writePointers(stateDir, rest);
10447
- else rmSync11(lastSessionFile(stateDir), { force: true });
10606
+ else rmSync12(lastSessionFile(stateDir), { force: true });
10448
10607
  } catch {
10449
10608
  }
10450
10609
  }
@@ -10472,15 +10631,15 @@ async function signOutSessions(o) {
10472
10631
  }
10473
10632
 
10474
10633
  // src/reset.ts
10475
- import { existsSync as existsSync22, mkdirSync as mkdirSync14, readFileSync as readFileSync21, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync7, writeFileSync as writeFileSync15 } from "fs";
10476
- import { dirname as dirname10, join as join24 } from "path";
10634
+ import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8, writeFileSync as writeFileSync16 } from "fs";
10635
+ import { dirname as dirname10, join as join25 } from "path";
10477
10636
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
10478
10637
  function clonedCheckoutsFile(stateDir) {
10479
- return join24(stateDir, "cloned-checkouts.json");
10638
+ return join25(stateDir, "cloned-checkouts.json");
10480
10639
  }
10481
10640
  function readClonedCheckouts(stateDir) {
10482
10641
  try {
10483
- const parsed = JSON.parse(readFileSync21(clonedCheckoutsFile(stateDir), "utf8"));
10642
+ const parsed = JSON.parse(readFileSync22(clonedCheckoutsFile(stateDir), "utf8"));
10484
10643
  return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
10485
10644
  } catch {
10486
10645
  return [];
@@ -10490,8 +10649,8 @@ function recordClonedCheckout(stateDir, dir) {
10490
10649
  const known = readClonedCheckouts(stateDir);
10491
10650
  if (known.includes(dir)) return;
10492
10651
  const file = clonedCheckoutsFile(stateDir);
10493
- mkdirSync14(dirname10(file), { recursive: true });
10494
- writeFileSync15(file, `${JSON.stringify([...known, dir], null, 2)}
10652
+ mkdirSync15(dirname10(file), { recursive: true });
10653
+ writeFileSync16(file, `${JSON.stringify([...known, dir], null, 2)}
10495
10654
  `);
10496
10655
  }
10497
10656
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -10543,8 +10702,8 @@ async function runReset(o, fx) {
10543
10702
  for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
10544
10703
  }
10545
10704
  steps.push(...removeState(o.stateDir, o.dryRun));
10546
- if (existsSync22(o.configFile)) {
10547
- if (!o.dryRun) rmSync12(o.configFile, { force: true });
10705
+ if (existsSync23(o.configFile)) {
10706
+ if (!o.dryRun) rmSync13(o.configFile, { force: true });
10548
10707
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
10549
10708
  } else {
10550
10709
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -10552,7 +10711,7 @@ async function runReset(o, fx) {
10552
10711
  return steps;
10553
10712
  }
10554
10713
  function removeCheckout(dir, dryRun, fx) {
10555
- if (!existsSync22(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
10714
+ if (!existsSync23(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
10556
10715
  const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
10557
10716
  if (dirty.length) {
10558
10717
  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" : ""})` };
@@ -10560,21 +10719,21 @@ function removeCheckout(dir, dryRun, fx) {
10560
10719
  if (linkedWorktrees.length) {
10561
10720
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
10562
10721
  }
10563
- if (!dryRun) rmSync12(dir, { recursive: true, force: true });
10722
+ if (!dryRun) rmSync13(dir, { recursive: true, force: true });
10564
10723
  return { step: "checkout", target: dir, action: mark(dryRun) };
10565
10724
  }
10566
10725
  function removeState(stateDir, dryRun) {
10567
10726
  const out = [];
10568
- const targets = [join24(stateDir, "resolved"), join24(stateDir, "keys"), clonedCheckoutsFile(stateDir), join24(stateDir, "connect-session.json"), join24(stateDir, "reload-requested"), join24(stateDir, "daemon.started")];
10727
+ const targets = [join25(stateDir, "resolved"), join25(stateDir, "keys"), clonedCheckoutsFile(stateDir), join25(stateDir, "connect-session.json"), join25(stateDir, "reload-requested"), join25(stateDir, "daemon.started")];
10569
10728
  try {
10570
10729
  for (const name of readdirSync9(stateDir)) {
10571
- if (name.startsWith(".") && statSync7(join24(stateDir, name)).isFile()) targets.push(join24(stateDir, name));
10730
+ if (name.startsWith(".") && statSync8(join25(stateDir, name)).isFile()) targets.push(join25(stateDir, name));
10572
10731
  }
10573
10732
  } catch {
10574
10733
  }
10575
10734
  for (const t of targets) {
10576
- if (!existsSync22(t)) continue;
10577
- if (!dryRun) rmSync12(t, { recursive: true, force: true });
10735
+ if (!existsSync23(t)) continue;
10736
+ if (!dryRun) rmSync13(t, { recursive: true, force: true });
10578
10737
  out.push({ step: "state", target: t, action: mark(dryRun) });
10579
10738
  }
10580
10739
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -10648,12 +10807,12 @@ async function checkCrewFloor(o, deps = {}) {
10648
10807
  function openOutdated(state) {
10649
10808
  return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
10650
10809
  }
10651
- function isUpgradeRequired(err2) {
10652
- return err2 instanceof TablationApiError3 && err2.status === 426;
10810
+ function isUpgradeRequired(err3) {
10811
+ return err3 instanceof TablationApiError3 && err3.status === 426;
10653
10812
  }
10654
- async function raiseFromUpgradeRequired(o, err2) {
10813
+ async function raiseFromUpgradeRequired(o, err3) {
10655
10814
  const have = sentVersion(o.ship) ?? "unknown";
10656
- const floor = /(\d+\.\d+\.\d+)/.exec(err2.message.replace(have, ""))?.[1] ?? "a newer version";
10815
+ const floor = /(\d+\.\d+\.\d+)/.exec(err3.message.replace(have, ""))?.[1] ?? "a newer version";
10657
10816
  await raiseShipAttention(o, outdatedItem(have, floor));
10658
10817
  }
10659
10818
 
@@ -11361,8 +11520,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
11361
11520
  }
11362
11521
 
11363
11522
  // src/poll.ts
11364
- import { writeFileSync as writeFileSync16 } from "fs";
11365
- import { join as join25 } from "path";
11523
+ import { writeFileSync as writeFileSync17 } from "fs";
11524
+ import { join as join26 } from "path";
11366
11525
 
11367
11526
  // src/stalled.ts
11368
11527
  var STALLED_AFTER_CYCLES = 30;
@@ -11468,8 +11627,8 @@ async function decideCycle(o) {
11468
11627
  const optional = async (what, read, fallback) => {
11469
11628
  try {
11470
11629
  return await read;
11471
- } catch (err2) {
11472
- emit.warn(`reading ${what} failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
11630
+ } catch (err3) {
11631
+ emit.warn(`reading ${what} failed: ${err3 instanceof Error ? err3.message : String(err3)}`);
11473
11632
  return fallback;
11474
11633
  }
11475
11634
  };
@@ -11669,7 +11828,7 @@ async function decideCycle(o) {
11669
11828
  }
11670
11829
  return decision;
11671
11830
  }
11672
- var digestPath = (stateDir, route2, role, runId) => join25(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
11831
+ var digestPath = (stateDir, route2, role, runId) => join26(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
11673
11832
  function otherRunsRemoved(tickets, own) {
11674
11833
  if (!own) return tickets;
11675
11834
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -11747,7 +11906,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
11747
11906
 
11748
11907
  ${body}` : body;
11749
11908
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
11750
- writeFileSync16(path, text);
11909
+ writeFileSync17(path, text);
11751
11910
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
11752
11911
  data: { role, bytes: Buffer.byteLength(text) }
11753
11912
  });
@@ -12230,13 +12389,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
12230
12389
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
12231
12390
 
12232
12391
  // src/connect-wizard.ts
12233
- import { delimiter, dirname as dirname11, join as join26 } from "path";
12234
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync23, mkdirSync as mkdirSync15, writeFileSync as writeFileSync17 } from "fs";
12392
+ import { delimiter, dirname as dirname11, join as join27 } from "path";
12393
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync24, mkdirSync as mkdirSync16, writeFileSync as writeFileSync18 } from "fs";
12235
12394
  import { stringify } from "yaml";
12236
12395
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
12237
12396
  function shouldRunWizard(opts) {
12238
12397
  if (!opts.isTTY) return false;
12239
- const exists = opts.configExists ?? existsSync23;
12398
+ const exists = opts.configExists ?? existsSync24;
12240
12399
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
12241
12400
  }
12242
12401
  function firstRunConfigPath() {
@@ -12244,7 +12403,7 @@ function firstRunConfigPath() {
12244
12403
  }
12245
12404
  function defaultIsExecutable(p) {
12246
12405
  try {
12247
- accessSync2(p, constants2.X_OK);
12406
+ accessSync3(p, constants3.X_OK);
12248
12407
  return true;
12249
12408
  } catch {
12250
12409
  return false;
@@ -12253,7 +12412,7 @@ function defaultIsExecutable(p) {
12253
12412
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
12254
12413
  for (const dir of pathEnv.split(delimiter)) {
12255
12414
  if (!dir) continue;
12256
- const candidate = join26(dir, name);
12415
+ const candidate = join27(dir, name);
12257
12416
  if (isExecutable(candidate)) return candidate;
12258
12417
  }
12259
12418
  return void 0;
@@ -12314,9 +12473,9 @@ routes:
12314
12473
  ${routeBlock}`;
12315
12474
  }
12316
12475
  function writeNewConfig(path, text) {
12317
- mkdirSync15(dirname11(path), { recursive: true });
12476
+ mkdirSync16(dirname11(path), { recursive: true });
12318
12477
  try {
12319
- writeFileSync17(path, text, { mode: 384, flag: "wx" });
12478
+ writeFileSync18(path, text, { mode: 384, flag: "wx" });
12320
12479
  return true;
12321
12480
  } catch (e) {
12322
12481
  if (e.code === "EEXIST") return false;
@@ -12348,9 +12507,9 @@ Wrote ${o.configPath}.
12348
12507
  }
12349
12508
 
12350
12509
  // src/connect-json.ts
12351
- import { accessSync as accessSync3, constants as constants3 } from "fs";
12510
+ import { accessSync as accessSync4, constants as constants4 } from "fs";
12352
12511
  import { homedir as homedir8 } from "os";
12353
- import { dirname as dirname12, join as join27 } from "path";
12512
+ import { dirname as dirname12, join as join28 } from "path";
12354
12513
  import { delimiter as delimiter2 } from "path";
12355
12514
  var CHOICE_EXIT_CODE = 3;
12356
12515
  function createEmitter(json, write) {
@@ -12389,12 +12548,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
12389
12548
  const dirs = [
12390
12549
  ...pathEnv.split(delimiter2),
12391
12550
  ...(extraPath ?? "").split(delimiter2),
12392
- join27(home, ".local/bin"),
12393
- join27(home, ".claude/local"),
12551
+ join28(home, ".local/bin"),
12552
+ join28(home, ".claude/local"),
12394
12553
  "/opt/homebrew/bin",
12395
12554
  "/usr/local/bin",
12396
- join27(home, ".npm-global/bin"),
12397
- join27(home, ".volta/bin")
12555
+ join28(home, ".npm-global/bin"),
12556
+ join28(home, ".volta/bin")
12398
12557
  ];
12399
12558
  return [...new Set(dirs.filter(Boolean))];
12400
12559
  }
@@ -12405,7 +12564,7 @@ function findAgentBin(name, env, extraPath, isExec) {
12405
12564
  function flagWizardAnswers(f, env) {
12406
12565
  const isExec = env.isExecutable ?? ((p) => {
12407
12566
  try {
12408
- accessSync3(p, constants3.X_OK);
12567
+ accessSync4(p, constants4.X_OK);
12409
12568
  return true;
12410
12569
  } catch {
12411
12570
  return false;
@@ -12430,8 +12589,8 @@ function flagWizardAnswers(f, env) {
12430
12589
  }
12431
12590
 
12432
12591
  // src/release-run.ts
12433
- import { readFileSync as readFileSync22, writeFileSync as writeFileSync18, existsSync as existsSync24, copyFileSync, mkdirSync as mkdirSync16, mkdtempSync as mkdtempSync2, rmSync as rmSync13 } from "fs";
12434
- import { dirname as dirname13, join as join28 } from "path";
12592
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync19, existsSync as existsSync25, copyFileSync, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, rmSync as rmSync14 } from "fs";
12593
+ import { dirname as dirname13, join as join29 } from "path";
12435
12594
  import { tmpdir as tmpdir3 } from "os";
12436
12595
 
12437
12596
  // src/release.ts
@@ -13172,13 +13331,13 @@ async function bump(o, size) {
13172
13331
  }
13173
13332
  return printed;
13174
13333
  }
13175
- const files = o.repo.release.versionFiles.map((f) => join28(o.cwd, f));
13334
+ const files = o.repo.release.versionFiles.map((f) => join29(o.cwd, f));
13176
13335
  const first = files[0];
13177
- if (!existsSync24(first)) {
13336
+ if (!existsSync25(first)) {
13178
13337
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
13179
13338
  return null;
13180
13339
  }
13181
- const current = JSON.parse(readFileSync22(first, "utf8")).version;
13340
+ const current = JSON.parse(readFileSync23(first, "utf8")).version;
13182
13341
  if (!current) {
13183
13342
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
13184
13343
  return null;
@@ -13191,27 +13350,27 @@ async function bump(o, size) {
13191
13350
  return version;
13192
13351
  }
13193
13352
  for (const f of files) {
13194
- const src = readFileSync22(f, "utf8");
13353
+ const src = readFileSync23(f, "utf8");
13195
13354
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
13196
13355
  if (out === src) {
13197
13356
  o.emit.error(`no version field in ${f}`);
13198
13357
  return null;
13199
13358
  }
13200
- writeFileSync18(f, out);
13359
+ writeFileSync19(f, out);
13201
13360
  }
13202
13361
  return version;
13203
13362
  }
13204
13363
  function writeChangelog(o, version, entries) {
13205
13364
  const file = o.repo.release.changelog;
13206
13365
  if (!file || entries.length === 0) return;
13207
- const path = join28(o.cwd, file);
13366
+ const path = join29(o.cwd, file);
13208
13367
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
13209
13368
  if (o.dryRun) {
13210
13369
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
13211
13370
  return;
13212
13371
  }
13213
- const existing = existsSync24(path) ? readFileSync22(path, "utf8") : "# Changelog\n";
13214
- writeFileSync18(path, insertChangelogSection(existing, section));
13372
+ const existing = existsSync25(path) ? readFileSync23(path, "utf8") : "# Changelog\n";
13373
+ writeFileSync19(path, insertChangelogSection(existing, section));
13215
13374
  }
13216
13375
  async function confirm(o, expected) {
13217
13376
  if (!o.repo.hooks.released) return void 0;
@@ -13294,7 +13453,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
13294
13453
  run2(cwd, ["worktree", "remove", "--force", dir]);
13295
13454
  } finally {
13296
13455
  try {
13297
- rmSync13(root, { recursive: true, force: true });
13456
+ rmSync14(root, { recursive: true, force: true });
13298
13457
  } finally {
13299
13458
  run2(cwd, ["worktree", "prune"]);
13300
13459
  run2(cwd, ["branch", "-D", branch]);
@@ -13304,8 +13463,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
13304
13463
  function openReleaseCheckout(o) {
13305
13464
  const { remote, base } = o.repo.branch;
13306
13465
  const branch = `crew/release-${Date.now()}`;
13307
- const root = mkdtempSync2(join28(tmpdir3(), "crew-release-"));
13308
- const dir = join28(root, "checkout");
13466
+ const root = mkdtempSync2(join29(tmpdir3(), "crew-release-"));
13467
+ const dir = join29(root, "checkout");
13309
13468
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
13310
13469
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
13311
13470
  try {
@@ -13315,10 +13474,10 @@ function openReleaseCheckout(o) {
13315
13474
  throw e;
13316
13475
  }
13317
13476
  for (const f of o.repo.worktrees.copy) {
13318
- const src = join28(o.cwd, f);
13319
- if (!existsSync24(src)) continue;
13320
- mkdirSync16(dirname13(join28(dir, f)), { recursive: true });
13321
- copyFileSync(src, join28(dir, f));
13477
+ const src = join29(o.cwd, f);
13478
+ if (!existsSync25(src)) continue;
13479
+ mkdirSync17(dirname13(join29(dir, f)), { recursive: true });
13480
+ copyFileSync(src, join29(dir, f));
13322
13481
  }
13323
13482
  return { dir, branch, close };
13324
13483
  }
@@ -14019,12 +14178,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
14019
14178
  }
14020
14179
 
14021
14180
  // src/live-runs.ts
14022
- import { existsSync as existsSync26, readdirSync as readdirSync11 } from "fs";
14023
- import { join as join30 } from "path";
14181
+ import { existsSync as existsSync27, readdirSync as readdirSync11 } from "fs";
14182
+ import { join as join31 } from "path";
14024
14183
 
14025
14184
  // src/interrupted.ts
14026
- import { existsSync as existsSync25, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
14027
- import { join as join29 } from "path";
14185
+ import { existsSync as existsSync26, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
14186
+ import { join as join30 } from "path";
14028
14187
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
14029
14188
  function interruptedComment(b) {
14030
14189
  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.";
@@ -14043,21 +14202,21 @@ var pidAlive = (pid) => {
14043
14202
  };
14044
14203
  function runFinished(streamPath) {
14045
14204
  const path = `${streamPath}.meta.json`;
14046
- if (!existsSync25(path)) return false;
14205
+ if (!existsSync26(path)) return false;
14047
14206
  try {
14048
- return JSON.parse(readFileSync23(path, "utf8")).running !== true;
14207
+ return JSON.parse(readFileSync24(path, "utf8")).running !== true;
14049
14208
  } catch {
14050
14209
  return true;
14051
14210
  }
14052
14211
  }
14053
14212
  function orphanedStreams(o) {
14054
- const dir = join29(o.stateDir, "streams");
14055
- if (!existsSync25(dir)) return [];
14213
+ const dir = join30(o.stateDir, "streams");
14214
+ if (!existsSync26(dir)) return [];
14056
14215
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
14057
14216
  const out = [];
14058
14217
  for (const name of readdirSync10(dir)) {
14059
14218
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14060
- const streamPath = join29(dir, name);
14219
+ const streamPath = join30(dir, name);
14061
14220
  if (runFinished(streamPath)) continue;
14062
14221
  const sidecar = readRunSidecar(streamPath);
14063
14222
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -14111,12 +14270,12 @@ async function reconcileRuns(o) {
14111
14270
 
14112
14271
  // src/live-runs.ts
14113
14272
  function liveRuns(stateDir, isAlive = runIsAlive) {
14114
- const dir = join30(stateDir, "streams");
14115
- if (!existsSync26(dir)) return [];
14273
+ const dir = join31(stateDir, "streams");
14274
+ if (!existsSync27(dir)) return [];
14116
14275
  const out = [];
14117
14276
  for (const name of readdirSync11(dir)) {
14118
14277
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14119
- const streamPath = join30(dir, name);
14278
+ const streamPath = join31(dir, name);
14120
14279
  if (runFinished(streamPath)) continue;
14121
14280
  const sidecar = readRunSidecar(streamPath);
14122
14281
  if (!sidecar || !isAlive(sidecar)) continue;
@@ -14450,7 +14609,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
14450
14609
  }
14451
14610
 
14452
14611
  // src/watch.ts
14453
- import { watch as watchFile, existsSync as existsSync27, statSync as statSync8, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
14612
+ import { watch as watchFile, existsSync as existsSync28, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
14454
14613
  var E = "\x1B[";
14455
14614
  var dim = (s) => `${E}2m${s}${E}0m`;
14456
14615
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -14551,8 +14710,8 @@ function renderHeaders(states, now = Date.now()) {
14551
14710
  return states.map((s) => renderHeader(s, now)).join("");
14552
14711
  }
14553
14712
  function readFrom(file, from) {
14554
- if (!existsSync27(file)) return { events: [], offset: from };
14555
- const size = statSync8(file).size;
14713
+ if (!existsSync28(file)) return { events: [], offset: from };
14714
+ const size = statSync9(file).size;
14556
14715
  if (size < from) return readFrom(file, 0);
14557
14716
  if (size === from) return { events: [], offset: from };
14558
14717
  const fd = openSync4(file, "r");
@@ -14794,19 +14953,19 @@ function nextRoles(decision, ran) {
14794
14953
  }
14795
14954
 
14796
14955
  // src/ship-name.ts
14797
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, renameSync as renameSync2, writeFileSync as writeFileSync20 } from "fs";
14956
+ import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, renameSync as renameSync3, writeFileSync as writeFileSync21 } from "fs";
14798
14957
  import { dirname as dirname15 } from "path";
14799
14958
  import { parseDocument as parseDocument2 } from "yaml";
14800
14959
 
14801
14960
  // src/machine-id.ts
14802
14961
  import { execFileSync as execFileSync5 } from "child_process";
14803
- import { createHash as createHash4 } from "crypto";
14804
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, writeFileSync as writeFileSync19 } from "fs";
14805
- import { dirname as dirname14, join as join31 } from "path";
14962
+ import { createHash as createHash5 } from "crypto";
14963
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
14964
+ import { dirname as dirname14, join as join32 } from "path";
14806
14965
  function machineIdPathFor(stateDir) {
14807
- return join31(stateDir, "machine-id");
14966
+ return join32(stateDir, "machine-id");
14808
14967
  }
14809
- var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14968
+ var sha256 = (s) => createHash5("sha256").update(s).digest("hex");
14810
14969
  function readRawMachineId(platform2 = process.platform) {
14811
14970
  try {
14812
14971
  if (platform2 === "darwin") {
@@ -14817,8 +14976,8 @@ function readRawMachineId(platform2 = process.platform) {
14817
14976
  return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
14818
14977
  }
14819
14978
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
14820
- if (existsSync28(p)) {
14821
- const id = readFileSync24(p, "utf8").trim();
14979
+ if (existsSync29(p)) {
14980
+ const id = readFileSync25(p, "utf8").trim();
14822
14981
  if (id) return id;
14823
14982
  }
14824
14983
  }
@@ -14829,15 +14988,15 @@ function readRawMachineId(platform2 = process.platform) {
14829
14988
  function ensureMachineId(stateDir, opts = {}) {
14830
14989
  if (opts.override?.trim()) return sha256(opts.override.trim());
14831
14990
  const path = machineIdPathFor(stateDir);
14832
- if (existsSync28(path)) {
14833
- const cached = readFileSync24(path, "utf8").trim();
14991
+ if (existsSync29(path)) {
14992
+ const cached = readFileSync25(path, "utf8").trim();
14834
14993
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
14835
14994
  }
14836
14995
  const raw = (opts.read ?? readRawMachineId)();
14837
14996
  if (!raw) return void 0;
14838
14997
  const hashed = sha256(raw);
14839
- mkdirSync17(dirname14(path), { recursive: true });
14840
- writeFileSync19(path, `${hashed}
14998
+ mkdirSync18(dirname14(path), { recursive: true });
14999
+ writeFileSync20(path, `${hashed}
14841
15000
  `, { mode: 384 });
14842
15001
  return hashed;
14843
15002
  }
@@ -14908,34 +15067,34 @@ async function syncRouteName(i) {
14908
15067
  return { status: "ok", ...base, action, name: i.local };
14909
15068
  }
14910
15069
  function readSyncedName(resolvedFile) {
14911
- if (!existsSync29(resolvedFile)) return void 0;
15070
+ if (!existsSync30(resolvedFile)) return void 0;
14912
15071
  try {
14913
- const v = JSON.parse(readFileSync25(resolvedFile, "utf8")).shipName;
15072
+ const v = JSON.parse(readFileSync26(resolvedFile, "utf8")).shipName;
14914
15073
  return typeof v === "string" ? v : void 0;
14915
15074
  } catch {
14916
15075
  return void 0;
14917
15076
  }
14918
15077
  }
14919
15078
  function writeSyncedName(resolvedFile, name) {
14920
- if (!existsSync29(resolvedFile)) return;
15079
+ if (!existsSync30(resolvedFile)) return;
14921
15080
  let raw;
14922
15081
  try {
14923
- raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
15082
+ raw = JSON.parse(readFileSync26(resolvedFile, "utf8"));
14924
15083
  } catch {
14925
15084
  return;
14926
15085
  }
14927
15086
  if (raw.shipName === name) return;
14928
15087
  raw.shipName = name;
14929
- mkdirSync18(dirname15(resolvedFile), { recursive: true });
15088
+ mkdirSync19(dirname15(resolvedFile), { recursive: true });
14930
15089
  const tmp = `${resolvedFile}.tmp${process.pid}`;
14931
- writeFileSync20(tmp, `${JSON.stringify(raw, null, 2)}
15090
+ writeFileSync21(tmp, `${JSON.stringify(raw, null, 2)}
14932
15091
  `);
14933
- renameSync2(tmp, resolvedFile);
15092
+ renameSync3(tmp, resolvedFile);
14934
15093
  }
14935
15094
  async function reconcileShipName(i) {
14936
15095
  let local = i.ship.name;
14937
15096
  try {
14938
- local = shipNameOfYaml(readFileSync25(i.configFile, "utf8")) ?? local;
15097
+ local = shipNameOfYaml(readFileSync26(i.configFile, "utf8")) ?? local;
14939
15098
  } catch {
14940
15099
  }
14941
15100
  const resolvedFile = resolvedPathFor(i.stateDir, i.route);
@@ -14959,8 +15118,8 @@ async function reconcileShipName(i) {
14959
15118
  }
14960
15119
  if (result.status !== "ok") return result;
14961
15120
  if (result.action === "adopt" || result.action === "row-wins") {
14962
- const text = readFileSync25(i.configFile, "utf8");
14963
- writeFileSync20(i.configFile, setShipNameInYaml(text, result.name));
15121
+ const text = readFileSync26(i.configFile, "utf8");
15122
+ writeFileSync21(i.configFile, setShipNameInYaml(text, result.name));
14964
15123
  i.ship.name = result.name;
14965
15124
  if (result.action === "row-wins") {
14966
15125
  i.emit?.warn(
@@ -15558,7 +15717,6 @@ ${opts.area ? ` area: "${opts.area}"
15558
15717
 
15559
15718
  // src/cli.ts
15560
15719
  import {
15561
- getSessionStore as getSessionStore3,
15562
15720
  hostFromUrl as hostFromUrl4,
15563
15721
  loginWithDeviceCode,
15564
15722
  DeviceLoginError
@@ -15610,7 +15768,7 @@ function describeCapability(i) {
15610
15768
  }
15611
15769
 
15612
15770
  // src/agents-sync-run.ts
15613
- import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync21 } from "fs";
15771
+ import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync22 } from "fs";
15614
15772
  import { dirname as dirname16 } from "path";
15615
15773
  async function runAgentsSync(target, o) {
15616
15774
  const dryRun = o.dryRun ?? false;
@@ -15644,12 +15802,12 @@ async function runAgentsSync(target, o) {
15644
15802
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
15645
15803
  if (!dryRun) {
15646
15804
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
15647
- const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync26(resolvedPath, "utf8")) : {};
15805
+ const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync27(resolvedPath, "utf8")) : {};
15648
15806
  raw.agentPersonas = personaResult.agentPersonas;
15649
15807
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
15650
15808
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
15651
- mkdirSync19(dirname16(resolvedPath), { recursive: true });
15652
- writeFileSync21(resolvedPath, `${JSON.stringify(raw, null, 2)}
15809
+ mkdirSync20(dirname16(resolvedPath), { recursive: true });
15810
+ writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
15653
15811
  `);
15654
15812
  if (target.resolved) {
15655
15813
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -15664,7 +15822,7 @@ async function runAgentsSync(target, o) {
15664
15822
  }
15665
15823
 
15666
15824
  // src/logbook.ts
15667
- import { createHash as createHash5 } from "crypto";
15825
+ import { createHash as createHash6 } from "crypto";
15668
15826
  import { TablationClient as TablationClient4 } from "@tablation/client";
15669
15827
  var LogbookError = class extends Error {
15670
15828
  };
@@ -15737,7 +15895,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
15737
15895
  };
15738
15896
  }
15739
15897
  const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
15740
- const actualSha = createHash5("sha256").update(prompt).digest("hex");
15898
+ const actualSha = createHash6("sha256").update(prompt).digest("hex");
15741
15899
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
15742
15900
  return {
15743
15901
  entry,
@@ -15753,7 +15911,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
15753
15911
 
15754
15912
  // src/repos-check.ts
15755
15913
  import { spawnSync as spawnSync6 } from "child_process";
15756
- import { readFileSync as readFileSync27 } from "fs";
15914
+ import { readFileSync as readFileSync28 } from "fs";
15757
15915
  import { basename as basename8 } from "path";
15758
15916
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
15759
15917
  function checkHookSyntax(script, shell) {
@@ -15794,7 +15952,7 @@ function checkRepoSpec(dir, opts = {}) {
15794
15952
  }
15795
15953
  let parsed;
15796
15954
  try {
15797
- parsed = parseRepoConfig(readFileSync27(file, "utf8"), file);
15955
+ parsed = parseRepoConfig(readFileSync28(file, "utf8"), file);
15798
15956
  } catch (e) {
15799
15957
  if (!(e instanceof RepoConfigError)) throw e;
15800
15958
  return { ...empty, file, error: e.message, exitCode: 1 };
@@ -15851,7 +16009,7 @@ import { isMap as isMap2, parseDocument as parseDocument4, YAMLMap } from "yaml"
15851
16009
 
15852
16010
  // src/repos-cmd.ts
15853
16011
  import { homedir as homedir9 } from "os";
15854
- import { basename as basename9, join as join32, resolve as resolve3 } from "path";
16012
+ import { basename as basename9, join as join33, resolve as resolve3 } from "path";
15855
16013
  import { isMap, isSeq, parseDocument as parseDocument3, stringify as stringify2 } from "yaml";
15856
16014
  var ReposError = class extends Error {
15857
16015
  };
@@ -16129,7 +16287,7 @@ function planRepoClone(i) {
16129
16287
  }
16130
16288
  const ship = doc.get("ship", true);
16131
16289
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
16132
- const target = i.dir ? expandDir(i.dir, i.base) : join32(expandDir(basePath, i.base), i.route.split("/")[0], name);
16290
+ const target = i.dir ? expandDir(i.dir, i.base) : join33(expandDir(basePath, i.base), i.route.split("/")[0], name);
16133
16291
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
16134
16292
  return { ...add, remote, target };
16135
16293
  }
@@ -16476,7 +16634,7 @@ async function runAgentSuggest(o) {
16476
16634
  const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
16477
16635
  const run2 = await new Promise((resolve6) => {
16478
16636
  let out = "";
16479
- let err2 = "";
16637
+ let err3 = "";
16480
16638
  const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
16481
16639
  let settled = false;
16482
16640
  const settle = (r) => {
@@ -16493,7 +16651,7 @@ async function runAgentSuggest(o) {
16493
16651
  }
16494
16652
  child.stdout.destroy();
16495
16653
  child.stderr.destroy();
16496
- settle({ code: null, out, err: err2, aborted: true });
16654
+ settle({ code: null, out, err: err3, aborted: true });
16497
16655
  };
16498
16656
  if (signal.aborted) onAbort();
16499
16657
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -16501,10 +16659,10 @@ async function runAgentSuggest(o) {
16501
16659
  out += d.toString();
16502
16660
  });
16503
16661
  child.stderr.on("data", (d) => {
16504
- err2 += d.toString();
16662
+ err3 += d.toString();
16505
16663
  });
16506
- child.on("error", (e) => settle({ code: null, out, err: err2, spawnError: e, aborted: false }));
16507
- child.on("close", (code) => settle({ code, out, err: err2, aborted: false }));
16664
+ child.on("error", (e) => settle({ code: null, out, err: err3, spawnError: e, aborted: false }));
16665
+ child.on("close", (code) => settle({ code, out, err: err3, aborted: false }));
16508
16666
  child.stdin.on("error", () => {
16509
16667
  });
16510
16668
  child.stdin.end(buildAgentPrompt(o.sniff));
@@ -16528,7 +16686,7 @@ ${run2.err}` });
16528
16686
  }
16529
16687
 
16530
16688
  // src/sample-ticket.ts
16531
- import { basename as basename10, dirname as dirname18, join as join33 } from "path";
16689
+ import { basename as basename10, dirname as dirname18, join as join34 } from "path";
16532
16690
  var SAMPLE_REPO = "crew-sample";
16533
16691
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
16534
16692
  var SAMPLE_MARKER = "sample: true";
@@ -16551,8 +16709,8 @@ has something safe to work on: its test and build hooks always pass, it has no
16551
16709
  remote, and \`crew sample-ticket --remove\` deletes it.
16552
16710
  `;
16553
16711
  function sampleDir(existing, reposBase, workspace) {
16554
- if (existing.length > 0) return join33(dirname18(existing[0]), SAMPLE_REPO);
16555
- return join33(reposBase, workspace, SAMPLE_REPO);
16712
+ if (existing.length > 0) return join34(dirname18(existing[0]), SAMPLE_REPO);
16713
+ return join34(reposBase, workspace, SAMPLE_REPO);
16556
16714
  }
16557
16715
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
16558
16716
  function sampleDescription() {
@@ -16644,8 +16802,8 @@ function schedulerInstalled(plan, exists, crontab) {
16644
16802
  }
16645
16803
 
16646
16804
  // src/worktree-sweep.ts
16647
- import { existsSync as existsSync31, readFileSync as readFileSync28, readdirSync as readdirSync12 } from "fs";
16648
- import { basename as basename11, dirname as dirname19, join as join34, resolve as resolve4 } from "path";
16805
+ import { existsSync as existsSync32, readFileSync as readFileSync29, readdirSync as readdirSync12 } from "fs";
16806
+ import { basename as basename11, dirname as dirname19, join as join35, resolve as resolve4 } from "path";
16649
16807
  function planWorktreeSweep(target, tickets, contract) {
16650
16808
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
16651
16809
  const verified = contract.statuses.verified;
@@ -16722,7 +16880,7 @@ function siblingWorktreeDirs(target) {
16722
16880
  for (const name of names) {
16723
16881
  if (!name.startsWith(stem)) continue;
16724
16882
  const m = DIR_NUMBER_SUFFIXED.exec(name);
16725
- const path = join34(parent, name);
16883
+ const path = join35(parent, name);
16726
16884
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
16727
16885
  }
16728
16886
  return out;
@@ -16757,7 +16915,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16757
16915
  let removed = 0;
16758
16916
  let keptBranches = 0;
16759
16917
  for (const a of actions) {
16760
- if (!dryRun && base && !a.keepBranch && existsSync31(a.path) && isHandBackMerge(a.path, base)) {
16918
+ if (!dryRun && base && !a.keepBranch && existsSync32(a.path) && isHandBackMerge(a.path, base)) {
16761
16919
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
16762
16920
  log.emit(
16763
16921
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -16765,7 +16923,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16765
16923
  );
16766
16924
  }
16767
16925
  }
16768
- if (!dryRun && existsSync31(a.path)) {
16926
+ if (!dryRun && existsSync32(a.path)) {
16769
16927
  let dirty = false;
16770
16928
  try {
16771
16929
  dirty = status(a.path).length > 0;
@@ -16776,7 +16934,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16776
16934
  continue;
16777
16935
  }
16778
16936
  }
16779
- if (!dryRun && a.requirePushed && a.branch && existsSync31(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
16937
+ if (!dryRun && a.requirePushed && a.branch && existsSync32(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
16780
16938
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
16781
16939
  continue;
16782
16940
  }
@@ -16825,7 +16983,7 @@ function isHandBackMerge(path, base) {
16825
16983
  if (!msgPath) return false;
16826
16984
  let first;
16827
16985
  try {
16828
- first = readFileSync28(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16986
+ first = readFileSync29(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16829
16987
  } catch {
16830
16988
  return false;
16831
16989
  }
@@ -16930,16 +17088,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
16930
17088
  }
16931
17089
 
16932
17090
  // src/stream-sweep.ts
16933
- import { existsSync as existsSync32, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9 } from "fs";
16934
- import { join as join35 } from "path";
17091
+ import { existsSync as existsSync33, readdirSync as readdirSync13, rmSync as rmSync15, statSync as statSync10 } from "fs";
17092
+ import { join as join36 } from "path";
16935
17093
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
16936
17094
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
16937
- const dir = join35(stateDir, "streams");
16938
- if (!existsSync32(dir)) return [];
17095
+ const dir = join36(stateDir, "streams");
17096
+ if (!existsSync33(dir)) return [];
16939
17097
  const cutoff = now - retentionDays * DAY_MS2;
16940
- return readdirSync13(dir).map((name) => join35(dir, name)).filter((path) => {
17098
+ return readdirSync13(dir).map((name) => join36(dir, name)).filter((path) => {
16941
17099
  try {
16942
- return statSync9(path).mtimeMs < cutoff;
17100
+ return statSync10(path).mtimeMs < cutoff;
16943
17101
  } catch {
16944
17102
  return false;
16945
17103
  }
@@ -16953,7 +17111,7 @@ function applyStreamSweep(paths, dryRun, log) {
16953
17111
  continue;
16954
17112
  }
16955
17113
  try {
16956
- rmSync14(path, { force: true });
17114
+ rmSync15(path, { force: true });
16957
17115
  removed++;
16958
17116
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
16959
17117
  } catch (e) {
@@ -16963,11 +17121,11 @@ function applyStreamSweep(paths, dryRun, log) {
16963
17121
  return removed;
16964
17122
  }
16965
17123
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16966
- if (!existsSync32(stateDir)) return [];
17124
+ if (!existsSync33(stateDir)) return [];
16967
17125
  const cutoff = now - retentionDays * DAY_MS2;
16968
- return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join35(stateDir, name)).filter((path) => {
17126
+ return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join36(stateDir, name)).filter((path) => {
16969
17127
  try {
16970
- return statSync9(path).mtimeMs < cutoff;
17128
+ return statSync10(path).mtimeMs < cutoff;
16971
17129
  } catch {
16972
17130
  return false;
16973
17131
  }
@@ -16975,30 +17133,30 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16975
17133
  }
16976
17134
 
16977
17135
  // src/cli.ts
16978
- import { realpathSync as realpathSync3, readFileSync as readFileSync34, existsSync as existsSync39, mkdirSync as mkdirSync25, writeFileSync as writeFileSync26, rmSync as rmSync18, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync13, readdirSync as readdirSync18 } from "fs";
17136
+ import { realpathSync as realpathSync3, readFileSync as readFileSync35, existsSync as existsSync40, mkdirSync as mkdirSync26, writeFileSync as writeFileSync27, rmSync as rmSync19, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync18 } from "fs";
16979
17137
  import { createInterface } from "readline/promises";
16980
17138
 
16981
17139
  // src/periscope-cmd.ts
16982
- import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, rmSync as rmSync15, writeFileSync as writeFileSync22 } from "fs";
16983
- import { join as join36 } from "path";
17140
+ import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, rmSync as rmSync16, writeFileSync as writeFileSync23 } from "fs";
17141
+ import { join as join37 } from "path";
16984
17142
  var PeriscopeError = class extends Error {
16985
17143
  };
16986
- var priorDir = (stateDir) => join36(stateDir, "periscope-priors");
16987
- var priorPath = (stateDir, ticketRecordId) => join36(priorDir(stateDir), `${ticketRecordId}.json`);
17144
+ var priorDir = (stateDir) => join37(stateDir, "periscope-priors");
17145
+ var priorPath = (stateDir, ticketRecordId) => join37(priorDir(stateDir), `${ticketRecordId}.json`);
16988
17146
  function removePeriscopePrior(stateDir, ticketRecordId) {
16989
- rmSync15(priorPath(stateDir, ticketRecordId), { force: true });
17147
+ rmSync16(priorPath(stateDir, ticketRecordId), { force: true });
16990
17148
  }
16991
17149
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
16992
17150
  try {
16993
- mkdirSync20(priorDir(stateDir), { recursive: true });
16994
- writeFileSync22(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
17151
+ mkdirSync21(priorDir(stateDir), { recursive: true });
17152
+ writeFileSync23(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
16995
17153
  } catch {
16996
17154
  }
16997
17155
  }
16998
17156
  function readPeriscopePrior(stateDir, ticketRecordId) {
16999
17157
  try {
17000
17158
  const p = priorPath(stateDir, ticketRecordId);
17001
- return existsSync33(p) ? JSON.parse(readFileSync29(p, "utf8")) : void 0;
17159
+ return existsSync34(p) ? JSON.parse(readFileSync30(p, "utf8")) : void 0;
17002
17160
  } catch {
17003
17161
  return void 0;
17004
17162
  }
@@ -17006,7 +17164,7 @@ function readPeriscopePrior(stateDir, ticketRecordId) {
17006
17164
  var DEFAULT_MAX_PERISCOPES2 = 3;
17007
17165
  var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
17008
17166
  function parsePeriscopeUpArgs(positional2, flags) {
17009
- const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--no-open] [--json]";
17167
+ const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--requested-by ID] [--no-open] [--json]";
17010
17168
  const rest = positional2.slice(2);
17011
17169
  if (rest.length > 2) throw new PeriscopeError(usage2);
17012
17170
  const topic = flags.topic?.trim();
@@ -17027,6 +17185,7 @@ ${usage2}`);
17027
17185
  isolate: flags.isolate,
17028
17186
  ...flags.host ? { host: flags.host } : {},
17029
17187
  ...flags.assignee ? { assignee: flags.assignee } : {},
17188
+ ...flags.requestedBy?.trim() ? { requestedBy: flags.requestedBy.trim() } : {},
17030
17189
  json: flags.json
17031
17190
  };
17032
17191
  }
@@ -17390,8 +17549,8 @@ ${tail2(preview.output)}`);
17390
17549
 
17391
17550
  // src/periscope-idle.ts
17392
17551
  import { execFileSync as execFileSync7 } from "child_process";
17393
- import { statSync as statSync10 } from "fs";
17394
- import { join as join37 } from "path";
17552
+ import { statSync as statSync11 } from "fs";
17553
+ import { join as join38 } from "path";
17395
17554
  var MAX_SCANNED_FILES = 2e4;
17396
17555
  function newestWorktreeMtime(root) {
17397
17556
  let listing;
@@ -17411,7 +17570,7 @@ function newestWorktreeMtime(root) {
17411
17570
  if (!rel) continue;
17412
17571
  if (++scanned > MAX_SCANNED_FILES) break;
17413
17572
  try {
17414
- const m = statSync10(join37(root, rel)).mtimeMs;
17573
+ const m = statSync11(join38(root, rel)).mtimeMs;
17415
17574
  if (newest === null || m > newest) newest = m;
17416
17575
  } catch {
17417
17576
  }
@@ -17487,18 +17646,18 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
17487
17646
 
17488
17647
  // src/ssh-keys.ts
17489
17648
  import { execFileSync as execFileSync8 } from "child_process";
17490
- import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, chmodSync as chmodSync2 } from "fs";
17491
- import { dirname as dirname20, join as join38 } from "path";
17649
+ import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, chmodSync as chmodSync3 } from "fs";
17650
+ import { dirname as dirname20, join as join39 } from "path";
17492
17651
  var SshKeyError = class extends Error {
17493
17652
  };
17494
17653
  function shipSshKeyPathFor(stateDir) {
17495
- return join38(stateDir, "ssh", "id_ed25519");
17654
+ return join39(stateDir, "ssh", "id_ed25519");
17496
17655
  }
17497
17656
  function ensureShipSshKeypair(stateDir) {
17498
17657
  const privateKeyPath = shipSshKeyPathFor(stateDir);
17499
17658
  const publicKeyPath = `${privateKeyPath}.pub`;
17500
- if (!existsSync34(privateKeyPath)) {
17501
- mkdirSync21(dirname20(privateKeyPath), { recursive: true });
17659
+ if (!existsSync35(privateKeyPath)) {
17660
+ mkdirSync22(dirname20(privateKeyPath), { recursive: true });
17502
17661
  try {
17503
17662
  execFileSync8("ssh-keygen", [
17504
17663
  "-t",
@@ -17513,15 +17672,15 @@ function ensureShipSshKeypair(stateDir) {
17513
17672
  "-q"
17514
17673
  ], { stdio: ["ignore", "ignore", "pipe"] });
17515
17674
  } catch (e) {
17516
- const err2 = e;
17517
- throw new SshKeyError(`ssh-keygen failed: ${err2.stderr?.toString().trim() || err2.message}`);
17675
+ const err3 = e;
17676
+ throw new SshKeyError(`ssh-keygen failed: ${err3.stderr?.toString().trim() || err3.message}`);
17518
17677
  }
17519
- chmodSync2(privateKeyPath, 384);
17678
+ chmodSync3(privateKeyPath, 384);
17520
17679
  }
17521
- if (!existsSync34(publicKeyPath)) {
17680
+ if (!existsSync35(publicKeyPath)) {
17522
17681
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
17523
17682
  }
17524
- const publicKey = readFileSync30(publicKeyPath, "utf8").trim();
17683
+ const publicKey = readFileSync31(publicKeyPath, "utf8").trim();
17525
17684
  return { privateKeyPath, publicKeyPath, publicKey };
17526
17685
  }
17527
17686
  function sshKeygenAvailable() {
@@ -17535,14 +17694,14 @@ function sshKeygenAvailable() {
17535
17694
 
17536
17695
  // src/passenger-containers.ts
17537
17696
  import { execFileSync as execFileSync9 } from "child_process";
17538
- import { createHash as createHash6 } from "crypto";
17697
+ import { createHash as createHash7 } from "crypto";
17539
17698
  var realExec = (cmd, args, opts) => {
17540
17699
  try {
17541
17700
  return execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
17542
17701
  } catch (e) {
17543
- const err2 = e;
17544
- const why = [err2.stderr, err2.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
17545
- throw new Error(`${cmd} ${args.join(" ")}: ${why || err2.message.trim()}`);
17702
+ const err3 = e;
17703
+ const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
17704
+ throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
17546
17705
  }
17547
17706
  };
17548
17707
  var DOCKER_INFO_TIMEOUT_MS = 1e4;
@@ -17573,12 +17732,12 @@ function containerNameFor(workspaceId) {
17573
17732
  return `crew-passenger-${workspaceId}`;
17574
17733
  }
17575
17734
  function portFor(workspaceId, base = 28765, span = 1e3) {
17576
- const digest = createHash6("sha256").update(workspaceId).digest();
17735
+ const digest = createHash7("sha256").update(workspaceId).digest();
17577
17736
  return base + digest.readUInt32BE(0) % span;
17578
17737
  }
17579
17738
  function mountsDigest(mounts, image) {
17580
17739
  const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
17581
- return createHash6("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
17740
+ return createHash7("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
17582
17741
  }
17583
17742
  function planContainers(cfg) {
17584
17743
  const byWorkspace = /* @__PURE__ */ new Map();
@@ -17738,9 +17897,9 @@ function syncAllPassengerCheckouts(cfg) {
17738
17897
 
17739
17898
  // src/tunnel.ts
17740
17899
  import { spawn as spawn5 } from "child_process";
17741
- import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, writeFileSync as writeFileSync23, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
17900
+ import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, writeFileSync as writeFileSync24, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
17742
17901
  import { randomInt } from "crypto";
17743
- import { join as join39 } from "path";
17902
+ import { join as join40 } from "path";
17744
17903
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
17745
17904
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
17746
17905
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -17807,21 +17966,21 @@ function startTunnel(opts) {
17807
17966
  };
17808
17967
  }
17809
17968
  function tunnelStatePath(stateDir, workspaceId) {
17810
- return join39(stateDir, "passengers", `${workspaceId}.json`);
17969
+ return join40(stateDir, "passengers", `${workspaceId}.json`);
17811
17970
  }
17812
17971
  function readPersistedTunnel(stateDir, workspaceId) {
17813
17972
  const path = tunnelStatePath(stateDir, workspaceId);
17814
- if (!existsSync35(path)) return void 0;
17973
+ if (!existsSync36(path)) return void 0;
17815
17974
  try {
17816
- return JSON.parse(readFileSync31(path, "utf8"));
17975
+ return JSON.parse(readFileSync32(path, "utf8"));
17817
17976
  } catch {
17818
17977
  return void 0;
17819
17978
  }
17820
17979
  }
17821
17980
  function writePersistedTunnel(stateDir, state) {
17822
17981
  const path = tunnelStatePath(stateDir, state.workspaceId);
17823
- mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
17824
- writeFileSync23(path, `${JSON.stringify(state, null, 2)}
17982
+ mkdirSync23(join40(stateDir, "passengers"), { recursive: true });
17983
+ writeFileSync24(path, `${JSON.stringify(state, null, 2)}
17825
17984
  `);
17826
17985
  }
17827
17986
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -17918,13 +18077,13 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
17918
18077
  }
17919
18078
  }
17920
18079
  function readAllPersistedWorkspaceIds(stateDir) {
17921
- const dir = join39(stateDir, "passengers");
17922
- if (!existsSync35(dir)) return [];
18080
+ const dir = join40(stateDir, "passengers");
18081
+ if (!existsSync36(dir)) return [];
17923
18082
  return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
17924
18083
  }
17925
18084
 
17926
18085
  // src/mcp-endpoint-registry.ts
17927
- import { createHash as createHash7 } from "crypto";
18086
+ import { createHash as createHash8 } from "crypto";
17928
18087
  function endpointsUrl(api, workspaceId, id) {
17929
18088
  const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
17930
18089
  return id ? `${base}/${id}` : base;
@@ -17992,7 +18151,7 @@ async function probeEndpointAccess(api, workspaceId) {
17992
18151
  }
17993
18152
  var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
17994
18153
  function registrationKey(url, mcpSecret) {
17995
- return createHash7("sha256").update(`${url}
18154
+ return createHash8("sha256").update(`${url}
17996
18155
  ${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
17997
18156
  }
17998
18157
  async function reconcileEndpoint(workspaceId, d) {
@@ -18040,8 +18199,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
18040
18199
  import { createServer as createHttpServer } from "http";
18041
18200
  import { spawn as spawn6 } from "child_process";
18042
18201
  import { randomBytes } from "crypto";
18043
- import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, writeFileSync as writeFileSync24, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
18044
- import { join as join40 } from "path";
18202
+ import { existsSync as existsSync37, mkdirSync as mkdirSync24, readFileSync as readFileSync33, writeFileSync as writeFileSync25, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
18203
+ import { join as join41 } from "path";
18045
18204
 
18046
18205
  // src/secret-compare.ts
18047
18206
  import { timingSafeEqual } from "crypto";
@@ -18115,8 +18274,8 @@ function runSyncDaemonFromEnv(env = process.env) {
18115
18274
  const secret = requireEnv(env, "PASSENGER_SYNC_SECRET");
18116
18275
  const targets = JSON.parse(requireEnv(env, "PASSENGER_SYNC_TARGETS"));
18117
18276
  const server = createSyncDaemonServer(targets, secret);
18118
- server.on("error", (err2) => {
18119
- console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err2 instanceof Error ? err2.message : String(err2)}`);
18277
+ server.on("error", (err3) => {
18278
+ console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err3 instanceof Error ? err3.message : String(err3)}`);
18120
18279
  process.exit(1);
18121
18280
  });
18122
18281
  server.listen(port, "0.0.0.0", () => {
@@ -18130,21 +18289,21 @@ var defaultSpawn2 = (cmd, args, env) => {
18130
18289
  return child;
18131
18290
  };
18132
18291
  function syncDaemonStatePath(stateDir, workspaceId) {
18133
- return join40(stateDir, "passengers", `${workspaceId}-sync.json`);
18292
+ return join41(stateDir, "passengers", `${workspaceId}-sync.json`);
18134
18293
  }
18135
18294
  function readPersistedSyncDaemon(stateDir, workspaceId) {
18136
18295
  const path = syncDaemonStatePath(stateDir, workspaceId);
18137
- if (!existsSync36(path)) return void 0;
18296
+ if (!existsSync37(path)) return void 0;
18138
18297
  try {
18139
- return JSON.parse(readFileSync32(path, "utf8"));
18298
+ return JSON.parse(readFileSync33(path, "utf8"));
18140
18299
  } catch {
18141
18300
  return void 0;
18142
18301
  }
18143
18302
  }
18144
18303
  function writePersistedSyncDaemon(stateDir, state) {
18145
18304
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
18146
- mkdirSync23(join40(stateDir, "passengers"), { recursive: true });
18147
- writeFileSync24(path, `${JSON.stringify(state, null, 2)}
18305
+ mkdirSync24(join41(stateDir, "passengers"), { recursive: true });
18306
+ writeFileSync25(path, `${JSON.stringify(state, null, 2)}
18148
18307
  `);
18149
18308
  }
18150
18309
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -18164,8 +18323,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
18164
18323
  }
18165
18324
  }
18166
18325
  function readAllPersistedSyncWorkspaceIds(stateDir) {
18167
- const dir = join40(stateDir, "passengers");
18168
- if (!existsSync36(dir)) return [];
18326
+ const dir = join41(stateDir, "passengers");
18327
+ if (!existsSync37(dir)) return [];
18169
18328
  const suffix = "-sync.json";
18170
18329
  return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
18171
18330
  }
@@ -18233,28 +18392,28 @@ function codeOf(e) {
18233
18392
  const code = e?.code;
18234
18393
  return typeof code === "string" ? code : void 0;
18235
18394
  }
18236
- function isTransientError(err2) {
18237
- if (!(err2 instanceof Error)) return false;
18238
- const msg = err2.message;
18395
+ function isTransientError(err3) {
18396
+ if (!(err3 instanceof Error)) return false;
18397
+ const msg = err3.message;
18239
18398
  if (msg === "fetch failed" || msg.startsWith("fetch failed")) return true;
18240
- for (let e = err2, depth = 0; e && depth < 4; e = e.cause, depth++) {
18399
+ for (let e = err3, depth = 0; e && depth < 4; e = e.cause, depth++) {
18241
18400
  const code = codeOf(e);
18242
18401
  if (code && NETWORK_CODES.includes(code)) return true;
18243
18402
  }
18244
18403
  if (NETWORK_CODES.some((c) => msg.includes(c))) return true;
18245
18404
  if (/\b(?:HTTP|status)\s*:?\s*(502|503|504)\b/i.test(msg) || /^(502|503|504)\b/.test(msg)) return true;
18246
- if (err2 instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
18405
+ if (err3 instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
18247
18406
  return false;
18248
18407
  }
18249
- function describeCause(err2) {
18250
- const first = (err2 instanceof Error ? err2.message : String(err2)).split("\n")[0];
18251
- const code = codeOf(err2?.cause);
18408
+ function describeCause(err3) {
18409
+ const first = (err3 instanceof Error ? err3.message : String(err3)).split("\n")[0];
18410
+ const code = codeOf(err3?.cause);
18252
18411
  return code && !first.includes(code) ? `${first} (${code})` : first;
18253
18412
  }
18254
18413
 
18255
18414
  // src/daemon.ts
18256
- import { existsSync as existsSync37, mkdirSync as mkdirSync24, readFileSync as readFileSync33, readdirSync as readdirSync16, rmSync as rmSync16, statSync as statSync11, writeFileSync as writeFileSync25 } from "fs";
18257
- import { join as join41 } from "path";
18415
+ import { existsSync as existsSync38, mkdirSync as mkdirSync25, readFileSync as readFileSync34, readdirSync as readdirSync16, rmSync as rmSync17, statSync as statSync12, writeFileSync as writeFileSync26 } from "fs";
18416
+ import { join as join42 } from "path";
18258
18417
  import { randomBytes as randomBytes2 } from "crypto";
18259
18418
  var DEFAULT_FLOOR_MS = 5e3;
18260
18419
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -18277,22 +18436,22 @@ async function runDaemonLoop(o) {
18277
18436
  o.onRecovered?.({ downMs: now() - downSince });
18278
18437
  transientRun = 0;
18279
18438
  }
18280
- } catch (err2) {
18281
- if (isTransientError(err2)) {
18439
+ } catch (err3) {
18440
+ if (isTransientError(err3)) {
18282
18441
  passes++;
18283
18442
  if (transientRun === 0) downSince = now();
18284
18443
  transientRun++;
18285
18444
  const wait = transientBackoffMs(transientRun);
18286
18445
  const downMs = now() - downSince;
18287
- o.onTransient?.({ err: err2, consecutive: transientRun, backoffMs: wait, downMs });
18288
- if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err: err2, consecutive: transientRun, downMs });
18446
+ o.onTransient?.({ err: err3, consecutive: transientRun, backoffMs: wait, downMs });
18447
+ if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err: err3, consecutive: transientRun, downMs });
18289
18448
  if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
18290
18449
  if (o.signal?.aborted) return "aborted";
18291
18450
  await sleep(wait);
18292
18451
  continue;
18293
18452
  }
18294
18453
  passes++;
18295
- o.onPassError?.(err2);
18454
+ o.onPassError?.(err3);
18296
18455
  if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
18297
18456
  if (o.signal?.aborted) return "aborted";
18298
18457
  await sleep(backoff);
@@ -18317,7 +18476,7 @@ async function runDaemonLoop(o) {
18317
18476
  function latestMtime(path) {
18318
18477
  let stat;
18319
18478
  try {
18320
- stat = statSync11(path);
18479
+ stat = statSync12(path);
18321
18480
  } catch {
18322
18481
  return 0;
18323
18482
  }
@@ -18325,7 +18484,7 @@ function latestMtime(path) {
18325
18484
  if (!stat.isDirectory()) return 0;
18326
18485
  let latest = stat.mtimeMs;
18327
18486
  for (const entry of readdirSync16(path, { withFileTypes: true })) {
18328
- latest = Math.max(latest, latestMtime(join41(path, entry.name)));
18487
+ latest = Math.max(latest, latestMtime(join42(path, entry.name)));
18329
18488
  }
18330
18489
  return latest;
18331
18490
  }
@@ -18334,23 +18493,23 @@ function computeTreeSignature(paths) {
18334
18493
  }
18335
18494
  function makeStaleChecker(paths, opts = {}) {
18336
18495
  const initial = computeTreeSignature(paths);
18337
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync37(opts.reloadSentinel);
18496
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync38(opts.reloadSentinel);
18338
18497
  }
18339
18498
  function reloadSentinelFor(stateDir) {
18340
- return join41(stateDir, "reload-requested");
18499
+ return join42(stateDir, "reload-requested");
18341
18500
  }
18342
18501
  function daemonStampFor(stateDir) {
18343
- return join41(stateDir, "daemon.started");
18502
+ return join42(stateDir, "daemon.started");
18344
18503
  }
18345
18504
  function noteDaemonStarted(stateDir, now = Date.now()) {
18346
- mkdirSync24(stateDir, { recursive: true });
18347
- rmSync16(reloadSentinelFor(stateDir), { force: true });
18348
- writeFileSync25(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
18505
+ mkdirSync25(stateDir, { recursive: true });
18506
+ rmSync17(reloadSentinelFor(stateDir), { force: true });
18507
+ writeFileSync26(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
18349
18508
  `);
18350
18509
  }
18351
18510
  function readDaemonStarted(stateDir) {
18352
18511
  try {
18353
- const v = JSON.parse(readFileSync33(daemonStampFor(stateDir), "utf8")).startedAt;
18512
+ const v = JSON.parse(readFileSync34(daemonStampFor(stateDir), "utf8")).startedAt;
18354
18513
  return typeof v === "number" ? v : null;
18355
18514
  } catch {
18356
18515
  return null;
@@ -18359,7 +18518,7 @@ function readDaemonStarted(stateDir) {
18359
18518
  function configChangedAfter(files, startedAt) {
18360
18519
  for (const f of files) {
18361
18520
  try {
18362
- if (statSync11(f).mtimeMs > startedAt) return f;
18521
+ if (statSync12(f).mtimeMs > startedAt) return f;
18363
18522
  } catch {
18364
18523
  }
18365
18524
  }
@@ -18411,7 +18570,7 @@ async function runOnePassUnguarded(o) {
18411
18570
  await hydrateApiKeys([o.route], o.getSessionStore);
18412
18571
  const emit = o.newEmitter();
18413
18572
  const decide = o.decide ?? decideCycle;
18414
- const spawn8 = o.spawnRoleAgent ?? runRoleAgent;
18573
+ const spawn9 = o.spawnRoleAgent ?? runRoleAgent;
18415
18574
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
18416
18575
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
18417
18576
  if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
@@ -18515,7 +18674,7 @@ async function runOnePassUnguarded(o) {
18515
18674
  const settled = new Promise((resolve6) => {
18516
18675
  ready = resolve6;
18517
18676
  });
18518
- const run2 = spawn8(role, decision, ctx, { ready }).then(() => {
18677
+ const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
18519
18678
  breaker.recordSuccess(role);
18520
18679
  clearShipAttention(o.state, `role_parked:${role}`);
18521
18680
  }).catch(async (e) => {
@@ -18659,6 +18818,23 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18659
18818
  const { route: route2, ship, emit } = ctx;
18660
18819
  const runId = randomBytes2(3).toString("hex");
18661
18820
  const identity = await resolveSeatKey(route2, role, ctx.getSessionStore);
18821
+ if (identity.kind === "ship" && identity.storeError && SEAT_KEY_ROLES.includes(role)) {
18822
+ emit.warn(`${role} run refused: its agent key cannot be read (${identity.storeError})`, { step: "agent", role });
18823
+ try {
18824
+ await raiseShipAttention(
18825
+ { state: ctx.state, emit, route: route2, ship },
18826
+ seatKeyUnreadableItem(routeSlug(route2.route), role, identity.storeError)
18827
+ );
18828
+ } catch {
18829
+ }
18830
+ return;
18831
+ }
18832
+ if (SEAT_KEY_ROLES.includes(role)) {
18833
+ try {
18834
+ clearShipAttention(ctx.state, seatKeyUnreadableKey(routeSlug(route2.route), role));
18835
+ } catch {
18836
+ }
18837
+ }
18662
18838
  if (identity.kind === "ship" && SEAT_KEY_ROLES.includes(role)) {
18663
18839
  emit.warn(`${role} writes as the ship, not its own agent: ${identity.reason}`, { step: "agent", role });
18664
18840
  } else if (identity.kind === "agent") {
@@ -18830,26 +19006,232 @@ async function buildEnvironment(route2, ship, ticket) {
18830
19006
  }
18831
19007
 
18832
19008
  // src/control-api.ts
18833
- import { createHash as createHash8 } from "crypto";
19009
+ import { createHash as createHash9 } from "crypto";
18834
19010
  import {
18835
- chmodSync as chmodSync3,
19011
+ chmodSync as chmodSync4,
18836
19012
  closeSync as closeSync5,
18837
- existsSync as existsSync38,
19013
+ existsSync as existsSync39,
18838
19014
  fstatSync,
18839
19015
  openSync as openSync5,
18840
19016
  readSync as readSync3,
18841
19017
  readdirSync as readdirSync17,
18842
- rmSync as rmSync17,
18843
- statSync as statSync12,
19018
+ rmSync as rmSync18,
19019
+ statSync as statSync13,
18844
19020
  watch
18845
19021
  } from "fs";
18846
19022
  import { createConnection } from "net";
18847
19023
  import { createServer } from "http";
18848
- import { basename as basename12, dirname as dirname21, join as join42 } from "path";
18849
- import { execFile, spawn as spawn7 } from "child_process";
19024
+ import { basename as basename12, dirname as dirname21, join as join43 } from "path";
19025
+ import { execFile, spawn as spawn8 } from "child_process";
19026
+
19027
+ // src/periscope-control.ts
19028
+ import { spawn as spawn7 } from "child_process";
19029
+ var reply = (status2, body) => ({ status: status2, body });
19030
+ var err = (status2, message) => reply(status2, { error: message });
19031
+ var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
19032
+ var MAX_FIELD = 200;
19033
+ function plainText(v, field, required) {
19034
+ if (v === void 0 || v === null || v === "") return required ? { error: `${field} is required` } : void 0;
19035
+ if (typeof v !== "string") return { error: `${field} must be a string` };
19036
+ const t = v.trim();
19037
+ if (!t) return required ? { error: `${field} is required` } : void 0;
19038
+ if (t.length > MAX_FIELD || /[\r\n\0]/.test(t)) return { error: `${field} must be one line of at most ${MAX_FIELD} characters` };
19039
+ if (t.startsWith("-")) return { error: `${field} must not start with "-"` };
19040
+ return t;
19041
+ }
19042
+ var isErr = (v) => typeof v === "object" && v !== null && "error" in v;
19043
+ function pickRoute(h, named) {
19044
+ const hosting = h.routes.filter((r) => r.hostPeriscopes);
19045
+ if (named !== void 0) {
19046
+ const hit = hosting.find((r) => r.route === named);
19047
+ return hit ?? err(404, "not found");
19048
+ }
19049
+ if (hosting.length === 1) return hosting[0];
19050
+ return err(400, `route is required (this ship hosts periscopes on: ${hosting.map((r) => r.route).join(", ")})`);
19051
+ }
19052
+ function cliMessage(r) {
19053
+ const lines = r.stderr.split("\n").map((l) => l.trim()).filter(Boolean);
19054
+ const plain = lines.filter((l) => !l.startsWith("{"));
19055
+ const last = plain[plain.length - 1] ?? lines[lines.length - 1] ?? `crew exited with status ${r.code}`;
19056
+ return last.replace(/^crew periscope:\s*/, "");
19057
+ }
19058
+ function failureStatus(r) {
19059
+ const m = cliMessage(r);
19060
+ if (/^no periscope for ticket /.test(m)) return 404;
19061
+ if (/already hosts \d+ live periscope/.test(m)) return 409;
19062
+ return r.code === 1 ? 422 : 500;
19063
+ }
19064
+ function lastJsonLine(stdout) {
19065
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
19066
+ for (let i = lines.length - 1; i >= 0; i--) {
19067
+ try {
19068
+ const v = JSON.parse(lines[i]);
19069
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
19070
+ } catch {
19071
+ }
19072
+ }
19073
+ return null;
19074
+ }
19075
+ function parseLine(line) {
19076
+ try {
19077
+ const v = JSON.parse(line);
19078
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
19079
+ } catch {
19080
+ return null;
19081
+ }
19082
+ }
19083
+ async function execute(h, args, streaming) {
19084
+ const finish = (r) => {
19085
+ if (r.code !== 0) return { status: failureStatus(r), body: { error: cliMessage(r) } };
19086
+ const done = lastJsonLine(r.stdout);
19087
+ return done ? { status: 200, body: done } : { status: 502, body: { error: "the command finished without a result" } };
19088
+ };
19089
+ if (!streaming) {
19090
+ const f = finish(await h.run(args, () => {
19091
+ }));
19092
+ return reply(f.status, f.body);
19093
+ }
19094
+ return {
19095
+ status: 200,
19096
+ body: null,
19097
+ stream: async (write) => {
19098
+ const r = await h.run(args, (line) => {
19099
+ const ev = parseLine(line);
19100
+ if (ev) write(ev);
19101
+ });
19102
+ const f = finish(r);
19103
+ write(f.status === 200 ? f.body : { event: "error", status: f.status, message: f.body.error });
19104
+ }
19105
+ };
19106
+ }
19107
+ var isPeriscopePath = (p) => p === "/periscopes" || p.startsWith("/periscopes/");
19108
+ async function handlePeriscopes(h, q) {
19109
+ if (!h || !h.routes.some((r) => r.hostPeriscopes)) return err(404, "not found");
19110
+ const down = /^\/periscopes\/([^/]+)\/down$/.exec(q.path);
19111
+ const isUp = q.path === "/periscopes" && q.method === "POST";
19112
+ const isList = q.path === "/periscopes" && q.method === "GET";
19113
+ if (q.path === "/periscopes" && !isUp && !isList) return err(405, "method not allowed");
19114
+ if (down && q.method !== "POST") return err(405, "method not allowed");
19115
+ if (q.path !== "/periscopes" && !down) return err(404, "not found");
19116
+ const member = async (workspaceId) => {
19117
+ const v = await h.verifyIn(q.token, workspaceId);
19118
+ if (v === "ok") return void 0;
19119
+ if (v === "unavailable") return err(503, "could not verify the key right now");
19120
+ return err(403, "this key does not belong to the workspace of this route");
19121
+ };
19122
+ if (isList) {
19123
+ const named = q.query.get("route") ?? void 0;
19124
+ const candidates2 = named === void 0 ? h.routes.filter((r) => r.hostPeriscopes) : [pickRoute(h, named)];
19125
+ const rows = [];
19126
+ for (const c of candidates2) {
19127
+ if ("status" in c) return c;
19128
+ const denied2 = await member(c.workspaceId);
19129
+ if (denied2) {
19130
+ if (named !== void 0) return denied2;
19131
+ continue;
19132
+ }
19133
+ const r = await h.run(["periscope", "list", c.route, "--json"], () => {
19134
+ });
19135
+ if (r.code !== 0) return err(failureStatus(r), cliMessage(r));
19136
+ try {
19137
+ const list = JSON.parse(r.stdout);
19138
+ if (Array.isArray(list)) for (const e of list) rows.push({ route: c.route, ...e });
19139
+ } catch {
19140
+ return err(502, "the list command did not answer JSON");
19141
+ }
19142
+ }
19143
+ return reply(200, { periscopes: rows });
19144
+ }
19145
+ const b = q.body;
19146
+ if (!b) return err(400, "body must be a small JSON object");
19147
+ const routeName = plainText(b.route, "route", false);
19148
+ if (isErr(routeName)) return err(400, routeName.error);
19149
+ const picked = pickRoute(h, routeName);
19150
+ if ("status" in picked) return picked;
19151
+ const denied = await member(picked.workspaceId);
19152
+ if (denied) return denied;
19153
+ if (down) {
19154
+ const ticket = decodeURIComponent(down[1]);
19155
+ if (!TICKET_RE.test(ticket)) return err(400, "ticket must be a ticket number such as 123 or CREW-123");
19156
+ if (b.outcome !== "keep" && b.outcome !== "discard") return err(400, 'outcome must be "keep" or "discard"');
19157
+ if (b.allowEmpty !== void 0 && typeof b.allowEmpty !== "boolean") return err(400, "allowEmpty must be a boolean");
19158
+ return execute(h, [
19159
+ "periscope",
19160
+ "down",
19161
+ picked.route,
19162
+ ticket,
19163
+ `--${b.outcome}`,
19164
+ ...b.allowEmpty === true ? ["--allow-empty"] : [],
19165
+ "--json"
19166
+ ], q.wantsStream);
19167
+ }
19168
+ const repo = plainText(b.repo, "repo", true);
19169
+ const topic = plainText(b.topic, "topic", true);
19170
+ const requestedBy = plainText(b.requestedBy, "requestedBy", false);
19171
+ for (const v of [repo, topic, requestedBy]) if (isErr(v)) return err(400, v.error);
19172
+ if (b.isolate !== void 0 && typeof b.isolate !== "boolean") return err(400, "isolate must be a boolean");
19173
+ if (!h.previewHost) return err(409, "this ship has no ship.previewHost, so a Passenger could not reach a periscope");
19174
+ const live = await h.run(["periscope", "list", picked.route, "--json"], () => {
19175
+ });
19176
+ if (live.code === 0) {
19177
+ try {
19178
+ const n = JSON.parse(live.stdout).length;
19179
+ if (n >= h.maxPeriscopes) return err(409, `this ship already hosts ${n} live periscope(s) (ship.maxPeriscopes is ${h.maxPeriscopes}) \u2014 discard one first`);
19180
+ } catch {
19181
+ }
19182
+ }
19183
+ return execute(h, [
19184
+ "periscope",
19185
+ "up",
19186
+ picked.route,
19187
+ repo,
19188
+ "--topic",
19189
+ topic,
19190
+ "--host",
19191
+ h.previewHost,
19192
+ ...requestedBy ? ["--requested-by", requestedBy] : [],
19193
+ ...b.isolate === true ? ["--isolate"] : [],
19194
+ "--no-open",
19195
+ "--json"
19196
+ ], q.wantsStream);
19197
+ }
19198
+ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
19199
+ return new Promise((resolve6) => {
19200
+ const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
19201
+ let stdout = "";
19202
+ let stderr = "";
19203
+ let pending = "";
19204
+ child.stdout.on("data", (d) => {
19205
+ stdout += d.toString("utf8");
19206
+ });
19207
+ child.stderr.on("data", (d) => {
19208
+ const text = d.toString("utf8");
19209
+ stderr += text;
19210
+ pending += text;
19211
+ let i;
19212
+ while ((i = pending.indexOf("\n")) >= 0) {
19213
+ const line = pending.slice(0, i).trim();
19214
+ pending = pending.slice(i + 1);
19215
+ if (line) onLine(line);
19216
+ }
19217
+ });
19218
+ const t = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
19219
+ child.on("error", (e) => {
19220
+ clearTimeout(t);
19221
+ resolve6({ code: 127, stdout, stderr: `${stderr}${e.message}
19222
+ ` });
19223
+ });
19224
+ child.on("close", (code) => {
19225
+ clearTimeout(t);
19226
+ resolve6({ code: code ?? 1, stdout, stderr });
19227
+ });
19228
+ });
19229
+ }
19230
+
19231
+ // src/control-api.ts
18850
19232
  var PAUSABLE = ["dev", "design", "qa"];
18851
19233
  var CONTROL_SOCKET_NAME = "control.sock";
18852
- var controlSocketFor = (stateDir) => join42(stateDir, CONTROL_SOCKET_NAME);
19234
+ var controlSocketFor = (stateDir) => join43(stateDir, CONTROL_SOCKET_NAME);
18853
19235
  var MAX_SOCKET_PATH = 100;
18854
19236
  var MAX_TAIL_BYTES = 1024 * 1024;
18855
19237
  var MAX_WAIT_SECONDS = 30;
@@ -18865,7 +19247,7 @@ var ControlAuth = class {
18865
19247
  if (!token) return "unauthorized";
18866
19248
  for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
18867
19249
  const now = (this.o.now ?? Date.now)();
18868
- const id = createHash8("sha256").update(token).digest("hex");
19250
+ const id = createHash9("sha256").update(token).digest("hex");
18869
19251
  const hit = this.cache.get(id);
18870
19252
  if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
18871
19253
  const verdict = await this.ask(token);
@@ -18877,11 +19259,26 @@ var ControlAuth = class {
18877
19259
  }
18878
19260
  return verdict;
18879
19261
  }
18880
- async ask(token) {
19262
+ /**
19263
+ * Like `verify`, but the token must belong to `workspaceId` itself, not just
19264
+ * to some workspace this ship serves. The ship's own keys pass.
19265
+ */
19266
+ async verifyIn(token, workspaceId) {
19267
+ if (!token) return "unauthorized";
19268
+ for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
19269
+ const now = (this.o.now ?? Date.now)();
19270
+ const id = `${workspaceId}:${createHash9("sha256").update(token).digest("hex")}`;
19271
+ const hit = this.cache.get(id);
19272
+ if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
19273
+ const verdict = await this.ask(token, [workspaceId]);
19274
+ if (verdict !== "unavailable") this.cache.set(id, { verdict, at: now });
19275
+ return verdict;
19276
+ }
19277
+ async ask(token, workspaceIds = this.o.workspaceIds) {
18881
19278
  const doFetch = this.o.fetch ?? fetch;
18882
19279
  const base = this.o.baseUrl.replace(/\/+$/, "");
18883
19280
  let forbidden = false;
18884
- for (const ws of this.o.workspaceIds) {
19281
+ for (const ws of workspaceIds) {
18885
19282
  let res;
18886
19283
  try {
18887
19284
  res = await doFetch(`${base}/api/auth/me?workspaceId=${encodeURIComponent(ws)}`, {
@@ -18920,7 +19317,7 @@ function candidates(live, rotated) {
18920
19317
  const out = [live];
18921
19318
  if (!rotated) return out;
18922
19319
  try {
18923
- for (const n of readdirSync17(dirname21(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join42(dirname21(live), n));
19320
+ for (const n of readdirSync17(dirname21(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join43(dirname21(live), n));
18924
19321
  } catch {
18925
19322
  }
18926
19323
  return out;
@@ -18943,7 +19340,7 @@ function readRange(path, from, max) {
18943
19340
  }
18944
19341
  }
18945
19342
  function lastLines2(path, n) {
18946
- const st = statSync12(path);
19343
+ const st = statSync13(path);
18947
19344
  const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
18948
19345
  if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
18949
19346
  let start = st.size;
@@ -18964,7 +19361,7 @@ function lastLines2(path, n) {
18964
19361
  }
18965
19362
  function readTail(live, opts) {
18966
19363
  const max = opts.maxBytes ?? MAX_TAIL_BYTES;
18967
- if (!existsSync38(live)) {
19364
+ if (!existsSync39(live)) {
18968
19365
  return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
18969
19366
  }
18970
19367
  const cur = opts.after ? decodeCursor(opts.after) : null;
@@ -18977,7 +19374,7 @@ function readTail(live, opts) {
18977
19374
  } else {
18978
19375
  const found = candidates(live, opts.rotated ?? false).find((p) => {
18979
19376
  try {
18980
- const s = statSync12(p);
19377
+ const s = statSync13(p);
18981
19378
  return s.ino === cur.ino && s.dev === cur.dev;
18982
19379
  } catch {
18983
19380
  return false;
@@ -18988,7 +19385,7 @@ function readTail(live, opts) {
18988
19385
  off = cur.off;
18989
19386
  } else reset = true;
18990
19387
  }
18991
- let size = statSync12(path).size;
19388
+ let size = statSync13(path).size;
18992
19389
  if (off > size) {
18993
19390
  off = 0;
18994
19391
  reset = true;
@@ -18996,12 +19393,12 @@ function readTail(live, opts) {
18996
19393
  if (off >= size && path !== live) {
18997
19394
  path = live;
18998
19395
  off = 0;
18999
- size = statSync12(path).size;
19396
+ size = statSync13(path).size;
19000
19397
  }
19001
19398
  const { text } = readRange(path, off, max);
19002
19399
  const cut = text.lastIndexOf("\n");
19003
19400
  const whole = cut < 0 ? "" : text.slice(0, cut + 1);
19004
- const st = statSync12(path);
19401
+ const st = statSync13(path);
19005
19402
  return {
19006
19403
  lines: whole.split("\n").filter((l) => l.length),
19007
19404
  cursor: encodeCursor({ dev: st.dev, ino: st.ino, off: off + Buffer.byteLength(whole) }),
@@ -19032,8 +19429,8 @@ function waitForChange(dir, ms3, signal) {
19032
19429
  }
19033
19430
  });
19034
19431
  }
19035
- var reply = (status2, body) => ({ status: status2, body });
19036
- var err = (status2, message) => reply(status2, { error: message });
19432
+ var reply2 = (status2, body) => ({ status: status2, body });
19433
+ var err2 = (status2, message) => reply2(status2, { error: message });
19037
19434
  function pauseDoc(state) {
19038
19435
  return {
19039
19436
  crew: state.isPaused(),
@@ -19073,26 +19470,38 @@ async function readJsonBody(req) {
19073
19470
  async function tailReply(path, q, rotated, abort) {
19074
19471
  const after = q.get("after") ?? void 0;
19075
19472
  const tail3 = q.has("tail") ? Number(q.get("tail")) : void 0;
19076
- if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err(400, "tail must be an integer from 0 to 1000");
19473
+ if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err2(400, "tail must be an integer from 0 to 1000");
19077
19474
  const wait = q.has("wait") ? Number(q.get("wait")) : 0;
19078
- if (!(Number.isFinite(wait) && wait >= 0)) return err(400, "wait must be a number of seconds");
19079
- if (after !== void 0 && !decodeCursor(after)) return err(400, "after is not a cursor this API issued");
19475
+ if (!(Number.isFinite(wait) && wait >= 0)) return err2(400, "wait must be a number of seconds");
19476
+ if (after !== void 0 && !decodeCursor(after)) return err2(400, "after is not a cursor this API issued");
19080
19477
  const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
19081
19478
  let r = readTail(path, { after, tail: tail3, rotated });
19082
19479
  while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
19083
19480
  await waitForChange(dirname21(path), deadline - Date.now(), abort);
19084
19481
  r = readTail(path, { after: r.cursor, rotated });
19085
19482
  }
19086
- return reply(200, r);
19483
+ return reply2(200, r);
19087
19484
  }
19088
19485
  async function route(deps, method, url, req, abort) {
19089
19486
  const p = url.pathname.replace(/\/+$/, "") || "/";
19090
19487
  const get2 = method === "GET";
19091
19488
  const post3 = method === "POST";
19489
+ if (isPeriscopePath(p)) {
19490
+ const token = /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1]?.trim() ?? "";
19491
+ const body = method === "POST" ? await readJsonBody(req) : {};
19492
+ return handlePeriscopes(deps.periscopes, {
19493
+ method,
19494
+ path: p,
19495
+ query: url.searchParams,
19496
+ token,
19497
+ body,
19498
+ wantsStream: url.searchParams.get("stream") === "1" || (req.headers.accept ?? "").includes("application/x-ndjson")
19499
+ });
19500
+ }
19092
19501
  if (p === "/status" && get2) {
19093
19502
  const doc = await deps.status();
19094
- if (!doc) return err(502, "could not produce the status document");
19095
- return reply(200, {
19503
+ if (!doc) return err2(502, "could not produce the status document");
19504
+ return reply2(200, {
19096
19505
  ...doc,
19097
19506
  daemon: {
19098
19507
  alive: true,
@@ -19104,47 +19513,56 @@ async function route(deps, method, url, req, abort) {
19104
19513
  });
19105
19514
  }
19106
19515
  if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
19107
- if (p === "/streams" && get2) return reply(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
19516
+ if (p === "/streams" && get2) return reply2(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
19108
19517
  if (p.startsWith("/streams/") && get2) {
19109
19518
  const run2 = decodeURIComponent(p.slice("/streams/".length));
19110
- if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err(400, "not a run name");
19111
- const file = join42(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
19112
- if (!existsSync38(file)) return err(404, "no such run");
19519
+ if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
19520
+ const file = join43(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
19521
+ if (!existsSync39(file)) return err2(404, "no such run");
19113
19522
  return tailReply(file, url.searchParams, false, abort);
19114
19523
  }
19115
19524
  if ((p === "/pause" || p === "/resume") && post3) {
19116
19525
  const body = await readJsonBody(req);
19117
- if (!body) return err(400, "body must be a small JSON object");
19526
+ if (!body) return err2(400, "body must be a small JSON object");
19118
19527
  const role = body.role;
19119
- if (role !== void 0 && !PAUSABLE.includes(role)) return err(400, `role must be one of ${PAUSABLE.join(", ")}`);
19528
+ if (role !== void 0 && !PAUSABLE.includes(role)) return err2(400, `role must be one of ${PAUSABLE.join(", ")}`);
19120
19529
  if (p === "/pause") deps.state.pause(role);
19121
19530
  else deps.state.resume(role);
19122
- return reply(200, { ok: true, pause: pauseDoc(deps.state) });
19531
+ return reply2(200, { ok: true, pause: pauseDoc(deps.state) });
19123
19532
  }
19124
19533
  if ((p === "/release/pause" || p === "/release/resume") && post3) {
19125
19534
  deps.state.setReleasePaused(p === "/release/pause");
19126
- return reply(200, { ok: true, pause: pauseDoc(deps.state) });
19535
+ return reply2(200, { ok: true, pause: pauseDoc(deps.state) });
19127
19536
  }
19128
19537
  const sched = /^\/scheduler\/(start|stop|restart)$/.exec(p);
19129
19538
  if (sched && post3) {
19130
19539
  const r = deps.scheduler(sched[1]);
19131
- return r.accepted ? reply(202, { accepted: true, detail: r.detail }) : err(409, r.detail);
19540
+ return r.accepted ? reply2(202, { accepted: true, detail: r.detail }) : err2(409, r.detail);
19132
19541
  }
19133
19542
  if (p === "/doctor" && get2) {
19134
19543
  const doc = await deps.doctor();
19135
- return doc ? reply(200, doc) : err(502, "could not produce the doctor report");
19544
+ return doc ? reply2(200, doc) : err2(502, "could not produce the doctor report");
19136
19545
  }
19137
19546
  const known = ["/status", "/events", "/streams", "/pause", "/resume", "/release/pause", "/release/resume", "/doctor"];
19138
- if (known.includes(p) || p.startsWith("/streams/") || sched) return err(405, "method not allowed");
19139
- return err(404, "not found");
19547
+ if (known.includes(p) || p.startsWith("/streams/") || sched) return err2(405, "method not allowed");
19548
+ return err2(404, "not found");
19140
19549
  }
19141
19550
  function send(res, r) {
19551
+ if (r.stream) {
19552
+ res.writeHead(200, { "Content-Type": "application/x-ndjson", "Cache-Control": "no-store" });
19553
+ return r.stream((ev) => {
19554
+ if (!res.writableEnded) res.write(`${JSON.stringify(ev)}
19555
+ `);
19556
+ }).then(() => {
19557
+ res.end();
19558
+ });
19559
+ }
19142
19560
  const body = JSON.stringify(r.body);
19143
19561
  res.writeHead(r.status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-store" });
19144
19562
  res.end(body);
19145
19563
  }
19146
19564
  async function socketIsStale(path) {
19147
- if (!existsSync38(path)) return false;
19565
+ if (!existsSync39(path)) return false;
19148
19566
  return new Promise((resolve6) => {
19149
19567
  const c = createConnection(path);
19150
19568
  c.once("connect", () => {
@@ -19162,8 +19580,8 @@ async function startControlServer(deps) {
19162
19580
  log(`control API not started: socket path is too long for a unix socket (${path})`);
19163
19581
  return null;
19164
19582
  }
19165
- if (await socketIsStale(path)) rmSync17(path, { force: true });
19166
- else if (existsSync38(path)) {
19583
+ if (await socketIsStale(path)) rmSync18(path, { force: true });
19584
+ else if (existsSync39(path)) {
19167
19585
  log(`control API not started: ${path} is in use by another process`);
19168
19586
  return null;
19169
19587
  }
@@ -19174,13 +19592,13 @@ async function startControlServer(deps) {
19174
19592
  try {
19175
19593
  const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
19176
19594
  const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
19177
- if (verdict === "unauthorized") return send(res, err(401, "invalid or revoked key"));
19178
- if (verdict === "forbidden") return send(res, err(403, "this key does not belong to a workspace this ship serves"));
19179
- if (verdict === "unavailable") return send(res, err(503, "could not verify the key right now"));
19180
- send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
19595
+ if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
19596
+ if (verdict === "forbidden") return send(res, err2(403, "this key does not belong to a workspace this ship serves"));
19597
+ if (verdict === "unavailable") return send(res, err2(503, "could not verify the key right now"));
19598
+ await send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
19181
19599
  } catch (e) {
19182
19600
  log(`control API request failed: ${e.message}`);
19183
- if (!res.headersSent) send(res, err(500, "internal error"));
19601
+ if (!res.headersSent) send(res, err2(500, "internal error"));
19184
19602
  else res.end();
19185
19603
  }
19186
19604
  })();
@@ -19192,7 +19610,7 @@ async function startControlServer(deps) {
19192
19610
  });
19193
19611
  server.listen(path, () => {
19194
19612
  try {
19195
- chmodSync3(path, 384);
19613
+ chmodSync4(path, 384);
19196
19614
  } catch {
19197
19615
  }
19198
19616
  server.on("error", (e) => log(`control API error: ${e.message}`));
@@ -19201,7 +19619,7 @@ async function startControlServer(deps) {
19201
19619
  path,
19202
19620
  close: () => new Promise((done) => {
19203
19621
  server.close(() => {
19204
- rmSync17(path, { force: true });
19622
+ rmSync18(path, { force: true });
19205
19623
  done();
19206
19624
  });
19207
19625
  })
@@ -19210,7 +19628,7 @@ async function startControlServer(deps) {
19210
19628
  });
19211
19629
  }
19212
19630
  var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
19213
- function runDetached(commands, spawnFn = spawn7) {
19631
+ function runDetached(commands, spawnFn = spawn8) {
19214
19632
  const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
19215
19633
  const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
19216
19634
  child.on("error", () => {
@@ -19678,7 +20096,7 @@ if (command === "passenger-sync-daemon") {
19678
20096
  } else {
19679
20097
  const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
19680
20098
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
19681
- if (configless && !existsSync39(findConfigFile(CREW_HOME))) {
20099
+ if (configless && !existsSync40(findConfigFile(CREW_HOME))) {
19682
20100
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
19683
20101
  } else {
19684
20102
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -19693,7 +20111,7 @@ if (command === "passenger-sync-daemon") {
19693
20111
  throw e;
19694
20112
  }
19695
20113
  if (command === "credential") {
19696
- const r = runCredential(positional[1], readFileSync34(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
20114
+ const r = runCredential(positional[1], readFileSync35(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
19697
20115
  process.stdout.write(r.out);
19698
20116
  process.stderr.write(r.err);
19699
20117
  process.exit(r.code);
@@ -19705,9 +20123,9 @@ if (command === "passenger-sync-daemon") {
19705
20123
  forges: cfg.ship.forges,
19706
20124
  stateDir: cfg.ship.stateDir,
19707
20125
  configFile: cfg.configFile,
19708
- readFile: (p) => readFileSync34(p, "utf8"),
19709
- writeFile: (p, t) => writeFileSync26(p, t),
19710
- stdin: () => readFileSync34(0, "utf8"),
20126
+ readFile: (p) => readFileSync35(p, "utf8"),
20127
+ writeFile: (p, t) => writeFileSync27(p, t),
20128
+ stdin: () => readFileSync35(0, "utf8"),
19711
20129
  env: process.env
19712
20130
  }
19713
20131
  );
@@ -19943,8 +20361,8 @@ if (command === "passenger-sync-daemon") {
19943
20361
  }
19944
20362
  if (host === "linux" && hasSystemd) {
19945
20363
  const label = labelFor(CREW_HOME, "run");
19946
- const staleTimer = join43(homedir10(), ".config", "systemd", "user", `${label}.timer`);
19947
- if (existsSync39(staleTimer)) {
20364
+ const staleTimer = join44(homedir10(), ".config", "systemd", "user", `${label}.timer`);
20365
+ if (existsSync40(staleTimer)) {
19948
20366
  process.stdout.write(
19949
20367
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
19950
20368
  `
@@ -19971,7 +20389,7 @@ if (command === "passenger-sync-daemon") {
19971
20389
  const tracker = new Tracker(c, cfg.ship);
19972
20390
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
19973
20391
  for (const r of await resolvedRepos(c)) {
19974
- if (!existsSync39(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
20392
+ if (!existsSync40(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
19975
20393
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
19976
20394
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
19977
20395
  }
@@ -19989,7 +20407,7 @@ if (command === "passenger-sync-daemon") {
19989
20407
  }
19990
20408
  async function doctorFix(o) {
19991
20409
  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))));
19992
- const unitExists = (p) => existsSync39(p);
20410
+ const unitExists = (p) => existsSync40(p);
19993
20411
  const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
19994
20412
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
19995
20413
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -20006,7 +20424,7 @@ if (command === "passenger-sync-daemon") {
20006
20424
  for (const a of plan.actions) {
20007
20425
  if (a.kind === "enable") {
20008
20426
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
20009
- writeFileSync26(cfg.configFile, planEnable(readFileSync34(cfg.configFile, "utf8"), a.route));
20427
+ writeFileSync27(cfg.configFile, planEnable(readFileSync35(cfg.configFile, "utf8"), a.route));
20010
20428
  enabledSound.add(a.route);
20011
20429
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
20012
20430
  } else {
@@ -20150,6 +20568,23 @@ ${changed2.map((c) => ` - ${c}
20150
20568
  }
20151
20569
  if (!dryRun && !c.enabled) return { scope, tests: "skipped", outcome: "skipped", detail: "route not enabled" };
20152
20570
  const releaseIdentity = await resolveReleaseKey(c);
20571
+ const releaseRouteSlug = routeSlug(c.route);
20572
+ if (releaseIdentity.kind === "ship" && releaseIdentity.storeError) {
20573
+ const detail = `release refused: its agent key cannot be read (${releaseIdentity.storeError})`;
20574
+ remit.warn(`${scope}: ${detail}`, { step: "release" });
20575
+ try {
20576
+ await raiseShipAttention(
20577
+ { state: new State(cfg.ship.stateDir), emit: remit, route: c, ship: cfg.ship },
20578
+ seatKeyUnreadableItem(releaseRouteSlug, "release", releaseIdentity.storeError)
20579
+ );
20580
+ } catch {
20581
+ }
20582
+ return { scope, tests: "skipped", outcome: "skipped", detail };
20583
+ }
20584
+ try {
20585
+ clearShipAttention(new State(cfg.ship.stateDir), seatKeyUnreadableKey(releaseRouteSlug, "release"));
20586
+ } catch {
20587
+ }
20153
20588
  if (releaseIdentity.kind === "ship") remit.warn(`release writes as the ship, not its own agent: ${releaseIdentity.reason}`, { step: "release" });
20154
20589
  const tracker = new Tracker(c, cfg.ship, { apiKey: releaseIdentity.apiKey });
20155
20590
  if (!dryRun) {
@@ -20599,8 +21034,8 @@ ${cu.tail.trimEnd()}
20599
21034
  `swept ${r.parked} parked, ${r.restored} restored${r.failed ? `, ${r.failed} failed` : ""}${r.contended ? `, ${r.contended} contended` : ""} (${e.route.route})`,
20600
21035
  { step: "sweep", data: r }
20601
21036
  );
20602
- } catch (err2) {
20603
- remit.warn(`could not sweep ${e.route.route}: ${err2.message}`, { step: "sweep" });
21037
+ } catch (err3) {
21038
+ remit.warn(`could not sweep ${e.route.route}: ${err3.message}`, { step: "sweep" });
20604
21039
  }
20605
21040
  }
20606
21041
  process.stdout.write(renderFleet(fleet, state));
@@ -21110,27 +21545,36 @@ ${cu.tail.trimEnd()}
21110
21545
  process.exit(1);
21111
21546
  }
21112
21547
  requireArmed("run the daemon loop");
21113
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join43(CREW_HOME, "src"), join43(CREW_HOME, "dist"), join43(CREW_HOME, "bin")];
21548
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join44(CREW_HOME, "src"), join44(CREW_HOME, "dist"), join44(CREW_HOME, "bin")];
21114
21549
  const configPaths = daemonConfigPaths(cfg);
21115
21550
  watchPaths.push(...configPaths);
21116
21551
  noteDaemonStarted(cfg.ship.stateDir);
21117
21552
  const spawnBreaker = new SpawnBreaker();
21118
21553
  const controlRoutes = cfg.routes.filter((r) => r.resolved?.workspaceId);
21554
+ const controlAuth = new ControlAuth({
21555
+ baseUrl: route2.baseUrl || DEFAULT_BASE_URL,
21556
+ workspaceIds: [...new Set(controlRoutes.map((r) => r.resolved.workspaceId))],
21557
+ shipKeys: () => cfg.routes.flatMap((r) => {
21558
+ try {
21559
+ return [resolveApiKey(r)];
21560
+ } catch {
21561
+ return [];
21562
+ }
21563
+ }),
21564
+ userAgent: cfg.ship.userAgent
21565
+ });
21119
21566
  const controlServer = await startControlServer({
21120
21567
  stateDir: cfg.ship.stateDir,
21121
21568
  state,
21122
- auth: new ControlAuth({
21123
- baseUrl: route2.baseUrl || DEFAULT_BASE_URL,
21124
- workspaceIds: [...new Set(controlRoutes.map((r) => r.resolved.workspaceId))],
21125
- shipKeys: () => cfg.routes.flatMap((r) => {
21126
- try {
21127
- return [resolveApiKey(r)];
21128
- } catch {
21129
- return [];
21130
- }
21131
- }),
21132
- userAgent: cfg.ship.userAgent
21133
- }),
21569
+ auth: controlAuth,
21570
+ // CREW-2859: Passenger periscopes, only on a route that sets hostPeriscopes.
21571
+ periscopes: {
21572
+ routes: controlRoutes.filter((r) => r.enabled).map((r) => ({ route: r.route, workspaceId: r.resolved.workspaceId, hostPeriscopes: r.hostPeriscopes === true })),
21573
+ previewHost: cfg.ship.previewHost,
21574
+ maxPeriscopes: cfg.ship.maxPeriscopes,
21575
+ run: runCrew,
21576
+ verifyIn: (token, ws) => controlAuth.verifyIn(token, ws)
21577
+ },
21134
21578
  status: () => crewJson(["status", route2.route, "--json"]),
21135
21579
  doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
21136
21580
  scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
@@ -21155,11 +21599,11 @@ ${cu.tail.trimEnd()}
21155
21599
  `)
21156
21600
  })
21157
21601
  }),
21158
- onTransient: ({ err: err2, consecutive, backoffMs }) => emit.warn(
21159
- `tracker unavailable (${describeCause(err2)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
21602
+ onTransient: ({ err: err3, consecutive, backoffMs }) => emit.warn(
21603
+ `tracker unavailable (${describeCause(err3)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
21160
21604
  ),
21161
- onOutage: ({ err: err2, downMs }) => {
21162
- const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err2)}`;
21605
+ onOutage: ({ err: err3, downMs }) => {
21606
+ const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err3)}`;
21163
21607
  emit.error(msg);
21164
21608
  void raiseShipAttention({ state, emit, route: route2, ship: cfg.ship }, {
21165
21609
  kind: "tracker_unreachable",
@@ -21172,8 +21616,8 @@ ${cu.tail.trimEnd()}
21172
21616
  clearShipAttention(state, TRACKER_OUTAGE_KEY);
21173
21617
  emit.emit(`tracker is back after ${Math.max(1, Math.round(downMs / 1e3))}s`);
21174
21618
  },
21175
- onPassError: (err2) => emit.error(
21176
- `daemon pass failed, continuing: ${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}`
21619
+ onPassError: (err3) => emit.error(
21620
+ `daemon pass failed, continuing: ${err3 instanceof Error ? err3.stack ?? err3.message : String(err3)}`
21177
21621
  ),
21178
21622
  checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
21179
21623
  onPass: (result) => {
@@ -21362,10 +21806,10 @@ ${cu.tail.trimEnd()}
21362
21806
  `);
21363
21807
  if (!dryRun) {
21364
21808
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
21365
- const raw = existsSync39(resolvedPath) ? JSON.parse(readFileSync34(resolvedPath, "utf8")) : {};
21809
+ const raw = existsSync40(resolvedPath) ? JSON.parse(readFileSync35(resolvedPath, "utf8")) : {};
21366
21810
  raw.agentSkills = result.agentSkills;
21367
- mkdirSync25(dirOf2(resolvedPath), { recursive: true });
21368
- writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
21811
+ mkdirSync26(dirOf2(resolvedPath), { recursive: true });
21812
+ writeFileSync27(resolvedPath, `${JSON.stringify(raw, null, 2)}
21369
21813
  `);
21370
21814
  }
21371
21815
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -21388,7 +21832,7 @@ ${cu.tail.trimEnd()}
21388
21832
  throw e;
21389
21833
  }
21390
21834
  const oldName = cfg.ship.name;
21391
- writeFileSync26(cfg.configFile, setShipNameInYaml(readFileSync34(cfg.configFile, "utf8"), newName));
21835
+ writeFileSync27(cfg.configFile, setShipNameInYaml(readFileSync35(cfg.configFile, "utf8"), newName));
21392
21836
  cfg.ship.name = newName;
21393
21837
  let renameMachineId;
21394
21838
  try {
@@ -21435,7 +21879,7 @@ ${cu.tail.trimEnd()}
21435
21879
  process.exit(2);
21436
21880
  };
21437
21881
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
21438
- const configText = readFileSync34(cfg.configFile, "utf8");
21882
+ const configText = readFileSync35(cfg.configFile, "utf8");
21439
21883
  if (sub === "check") {
21440
21884
  try {
21441
21885
  const routeArg = value("route") ?? positional[2];
@@ -21466,8 +21910,8 @@ ${cu.tail.trimEnd()}
21466
21910
  try {
21467
21911
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
21468
21912
  const dir = repoDirOf(configText, dirname22(cfg.configFile), a.route, a.name);
21469
- if (!existsSync39(dir)) throw new ReposError(`${dir} does not exist on disk`);
21470
- const files = { exists: (r) => existsSync39(resolvePath(dir, r)), list: (r) => {
21913
+ if (!existsSync40(dir)) throw new ReposError(`${dir} does not exist on disk`);
21914
+ const files = { exists: (r) => existsSync40(resolvePath(dir, r)), list: (r) => {
21471
21915
  try {
21472
21916
  return readdirSync18(resolvePath(dir, r));
21473
21917
  } catch {
@@ -21475,7 +21919,7 @@ ${cu.tail.trimEnd()}
21475
21919
  }
21476
21920
  }, read: (r) => {
21477
21921
  try {
21478
- return readFileSync34(resolvePath(dir, r), "utf8");
21922
+ return readFileSync35(resolvePath(dir, r), "utf8");
21479
21923
  } catch {
21480
21924
  return void 0;
21481
21925
  }
@@ -21517,19 +21961,19 @@ ${cu.tail.trimEnd()}
21517
21961
  let cleared;
21518
21962
  if (clear2) {
21519
21963
  const plan = clearRouteHooks(configText, a.route, a.name);
21520
- if (plan.removed.length > 0) writeFileSync26(cfg.configFile, plan.text);
21964
+ if (plan.removed.length > 0) writeFileSync27(cfg.configFile, plan.text);
21521
21965
  cleared = { file: cfg.configFile, removed: plan.removed };
21522
21966
  }
21523
21967
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
21524
21968
  if (write === "route") {
21525
21969
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
21526
- writeFileSync26(cfg.configFile, plan.text);
21970
+ writeFileSync27(cfg.configFile, plan.text);
21527
21971
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
21528
21972
  } else if (write === "repo") {
21529
21973
  const have = existingRepoConfig(files);
21530
21974
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
21531
21975
  const file = resolvePath(dir, ".crew.yaml");
21532
- writeFileSync26(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
21976
+ writeFileSync27(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
21533
21977
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
21534
21978
  }
21535
21979
  if (flag("json")) {
@@ -21585,7 +22029,7 @@ ${cu.tail.trimEnd()}
21585
22029
  if (sub === "list" && !routeName) usage2();
21586
22030
  if (sub === "add") {
21587
22031
  try {
21588
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync39(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
22032
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync40(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
21589
22033
  routeName = a.route;
21590
22034
  addPath = a.path;
21591
22035
  addName = a.name;
@@ -21614,16 +22058,16 @@ ${cu.tail.trimEnd()}
21614
22058
  }
21615
22059
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
21616
22060
  let resolvedRemotes;
21617
- if (existsSync39(resolvedFile)) {
22061
+ if (existsSync40(resolvedFile)) {
21618
22062
  try {
21619
- resolvedRemotes = JSON.parse(readFileSync34(resolvedFile, "utf8"));
22063
+ resolvedRemotes = JSON.parse(readFileSync35(resolvedFile, "utf8"));
21620
22064
  } catch {
21621
22065
  }
21622
22066
  }
21623
22067
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
21624
22068
  const text = configText;
21625
22069
  const base = dirname22(cfg.configFile);
21626
- const isCheckout = (d) => existsSync39(`${d}/.git`);
22070
+ const isCheckout = (d) => existsSync40(`${d}/.git`);
21627
22071
  try {
21628
22072
  if (sub === "list") {
21629
22073
  if (flag("json")) {
@@ -21633,11 +22077,11 @@ ${cu.tail.trimEnd()}
21633
22077
  base,
21634
22078
  resolved: resolvedRemotes,
21635
22079
  originOf,
21636
- exists: (d) => existsSync39(d),
22080
+ exists: (d) => existsSync40(d),
21637
22081
  isCheckout,
21638
22082
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
21639
22083
  try {
21640
- return Object.keys(parseRepoConfig(readFileSync34(resolvePath(d, f), "utf8"), f).hooks).length > 0;
22084
+ return Object.keys(parseRepoConfig(readFileSync35(resolvePath(d, f), "utf8"), f).hooks).length > 0;
21641
22085
  } catch {
21642
22086
  return false;
21643
22087
  }
@@ -21654,7 +22098,7 @@ ${cu.tail.trimEnd()}
21654
22098
  for (const r of repos) {
21655
22099
  const origin2 = originOf(r.dir);
21656
22100
  const row = matchRepoName(origin2, resolvedRemotes);
21657
- const note = !existsSync39(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
22101
+ const note = !existsSync40(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
21658
22102
  process.stdout.write(`${r.name} ${r.dir} ${note}
21659
22103
  `);
21660
22104
  }
@@ -21666,9 +22110,9 @@ ${cu.tail.trimEnd()}
21666
22110
  ` : `${human}
21667
22111
  `);
21668
22112
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
21669
- if (existsSync39(plan2.target) && (existsSync39(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
22113
+ if (existsSync40(plan2.target) && (existsSync40(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
21670
22114
  throw new ReposError(
21671
- existsSync39(`${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`
22115
+ existsSync40(`${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`
21672
22116
  );
21673
22117
  }
21674
22118
  if (dryRun) {
@@ -21686,14 +22130,14 @@ ${cu.tail.trimEnd()}
21686
22130
  throw new ReposError(`${e.message}${authHint}
21687
22131
  crew.yaml was not changed`);
21688
22132
  }
21689
- writeFileSync26(cfg.configFile, plan2.text);
22133
+ writeFileSync27(cfg.configFile, plan2.text);
21690
22134
  if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
21691
22135
  `);
21692
22136
  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}`);
21693
22137
  break;
21694
22138
  }
21695
22139
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
21696
- const top = existsSync39(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
22140
+ const top = existsSync40(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
21697
22141
  if (!top) throw new ReposError(`${target} is not a git checkout`);
21698
22142
  const origin = originOf(top);
21699
22143
  const plan = planRepoAdd({
@@ -21745,15 +22189,15 @@ ${cu.tail.trimEnd()}
21745
22189
  const created = await rowTracker.createRepoRow(rowPayload);
21746
22190
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
21747
22191
  `);
21748
- if (existsSync39(resolvedFile)) {
21749
- const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
22192
+ if (existsSync40(resolvedFile)) {
22193
+ const raw = JSON.parse(readFileSync35(resolvedFile, "utf8"));
21750
22194
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
21751
22195
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
21752
- writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
22196
+ writeFileSync27(resolvedFile, `${JSON.stringify(raw, null, 2)}
21753
22197
  `);
21754
22198
  }
21755
22199
  }
21756
- writeFileSync26(cfg.configFile, plan.text);
22200
+ writeFileSync27(cfg.configFile, plan.text);
21757
22201
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
21758
22202
  `);
21759
22203
  } catch (e) {
@@ -21852,7 +22296,7 @@ ${reconstruction.prompt}
21852
22296
  if (flag("sign-out")) {
21853
22297
  let signOutStore;
21854
22298
  try {
21855
- signOutStore = getSessionStore3();
22299
+ signOutStore = getSessionStore();
21856
22300
  } catch {
21857
22301
  signOutStore = void 0;
21858
22302
  }
@@ -21892,7 +22336,7 @@ ${reconstruction.prompt}
21892
22336
  const givenKeySource = value("key") ?? process.env.CREW_CONNECT_KEY ? "flag" : "config";
21893
22337
  let store;
21894
22338
  try {
21895
- store = getSessionStore3();
22339
+ store = getSessionStore();
21896
22340
  } catch {
21897
22341
  store = void 0;
21898
22342
  }
@@ -22120,7 +22564,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22120
22564
  process.exit(2);
22121
22565
  }
22122
22566
  if (found.shipNameAdopted && !dryRun) {
22123
- writeFileSync26(cfg.configFile, setShipNameInYaml(readFileSync34(cfg.configFile, "utf8"), found.shipNameAdopted));
22567
+ writeFileSync27(cfg.configFile, setShipNameInYaml(readFileSync35(cfg.configFile, "utf8"), found.shipNameAdopted));
22124
22568
  cfg.ship.name = found.shipNameAdopted;
22125
22569
  }
22126
22570
  if (found.shipRow) {
@@ -22139,8 +22583,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22139
22583
  }
22140
22584
  const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
22141
22585
  if (found.locksModelId && !dryRun) {
22142
- const configured = cfg.routes.find((r) => r.route === route3);
22143
- const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
22586
+ const configured2 = cfg.routes.find((r) => r.route === route3);
22587
+ const repoNames = [...Object.values(found.repoNames ?? {}), ...configured2 ? reposOf(configured2).map((t) => t.name) : []].filter((n) => !!n);
22144
22588
  try {
22145
22589
  const made = await provisionReleaseLocks(authOpts, found.locksModelId, route3, repoNames);
22146
22590
  for (const sc of made) process.stderr.write(`created release lock row ${sc}
@@ -22163,10 +22607,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22163
22607
  }
22164
22608
  }
22165
22609
  let previous;
22166
- const hadPreviousFile = existsSync39(resolvedPath);
22610
+ const hadPreviousFile = existsSync40(resolvedPath);
22167
22611
  if (hadPreviousFile) {
22168
22612
  try {
22169
- previous = JSON.parse(readFileSync34(resolvedPath, "utf8"));
22613
+ previous = JSON.parse(readFileSync35(resolvedPath, "utf8"));
22170
22614
  } catch {
22171
22615
  }
22172
22616
  }
@@ -22295,12 +22739,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22295
22739
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
22296
22740
  `);
22297
22741
  } else {
22298
- mkdirSync25(dirname22(resolvedPath), { recursive: true });
22742
+ mkdirSync26(dirname22(resolvedPath), { recursive: true });
22299
22743
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
22300
22744
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
22301
- writeFileSync26(tmpPath, `${JSON.stringify(resolved, null, 2)}
22745
+ writeFileSync27(tmpPath, `${JSON.stringify(resolved, null, 2)}
22302
22746
  `);
22303
- renameSync3(tmpPath, resolvedPath);
22747
+ renameSync4(tmpPath, resolvedPath);
22304
22748
  process.stderr.write(`Resolved ids written to ${resolvedPath}
22305
22749
  `);
22306
22750
  }
@@ -22344,8 +22788,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22344
22788
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
22345
22789
  `);
22346
22790
  } else {
22347
- mkdirSync25(dirname22(apiKeyFile), { recursive: true });
22348
- writeFileSync26(apiKeyFile, `${apiKeyVar}=${apiKey}
22791
+ mkdirSync26(dirname22(apiKeyFile), { recursive: true });
22792
+ writeFileSync27(apiKeyFile, `${apiKeyVar}=${apiKey}
22349
22793
  `, { mode: 384 });
22350
22794
  process.stderr.write(`API key written to ${apiKeyFile}
22351
22795
  `);
@@ -23079,7 +23523,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
23079
23523
  const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
23080
23524
  const resumed = await runPeriscopeResume({
23081
23525
  find: async () => hit && { row: hit.row, tag: hit.tag },
23082
- worktreeExists: (p) => existsSync39(p),
23526
+ worktreeExists: (p) => existsSync40(p),
23083
23527
  caps: periscopeCaps(cfg.ship),
23084
23528
  runHook: async (name, cwd, env) => {
23085
23529
  const script = hit?.repo.config.hooks[name];
@@ -23098,7 +23542,7 @@ ${resumed.url}
23098
23542
  `);
23099
23543
  break;
23100
23544
  }
23101
- const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
23545
+ const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), requestedBy: value("requested-by"), isolate: flag("isolate"), json: jsonP });
23102
23546
  let a;
23103
23547
  if (parsedUp.repo !== void 0) a = { ...parsedUp, repo: parsedUp.repo };
23104
23548
  else {
@@ -23156,7 +23600,7 @@ ${where.join("\n")}`);
23156
23600
  title,
23157
23601
  description: `A periscope: a throwaway preview of ${a.repo} for a viewer, opened by \`crew periscope up\`.`,
23158
23602
  report_type: "feature",
23159
- reporter_name: operatorName,
23603
+ reporter_name: a.requestedBy ?? operatorName,
23160
23604
  [cP.columns.status]: cP.statuses.draft,
23161
23605
  [cP.columns.assignee]: pairId,
23162
23606
  ...routeP.resolved.areaId ? { [cP.columns.slice]: routeP.resolved.areaId } : {},
@@ -23173,10 +23617,10 @@ ${where.join("\n")}`);
23173
23617
  },
23174
23618
  prepare: async (path) => {
23175
23619
  for (const f of repoP.config.worktrees.copy) {
23176
- const src = join43(repoP.dir, f);
23177
- if (!existsSync39(src)) continue;
23178
- mkdirSync25(dirOf2(join43(path, f)), { recursive: true });
23179
- copyFileSync2(src, join43(path, f));
23620
+ const src = join44(repoP.dir, f);
23621
+ if (!existsSync40(src)) continue;
23622
+ mkdirSync26(dirOf2(join44(path, f)), { recursive: true });
23623
+ copyFileSync2(src, join44(path, f));
23180
23624
  }
23181
23625
  const setup = repoP.config.hooks.setup;
23182
23626
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -23402,7 +23846,7 @@ ${where.join("\n")}`);
23402
23846
  const host = hostPlatform();
23403
23847
  let resetStore;
23404
23848
  try {
23405
- resetStore = getSessionStore3();
23849
+ resetStore = getSessionStore();
23406
23850
  } catch {
23407
23851
  resetStore = void 0;
23408
23852
  }
@@ -23451,7 +23895,7 @@ ${where.join("\n")}`);
23451
23895
  };
23452
23896
  const droppedRoutes = [];
23453
23897
  try {
23454
- const rawCfg = parseYaml2(readFileSync34(cfg.configFile, "utf8"));
23898
+ const rawCfg = parseYaml2(readFileSync35(cfg.configFile, "utf8"));
23455
23899
  for (const r of rawCfg?.routes ?? []) {
23456
23900
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
23457
23901
  try {
@@ -23493,8 +23937,8 @@ ${where.join("\n")}`);
23493
23937
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
23494
23938
  break;
23495
23939
  }
23496
- mkdirSync25(cfg.ship.stateDir, { recursive: true });
23497
- writeFileSync26(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
23940
+ mkdirSync26(cfg.ship.stateDir, { recursive: true });
23941
+ writeFileSync27(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
23498
23942
  `);
23499
23943
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
23500
23944
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -23545,7 +23989,7 @@ ${where.join("\n")}`);
23545
23989
  }
23546
23990
  case "log": {
23547
23991
  try {
23548
- const text = readFileSync34(cfg.ship.logFile, "utf8").split("\n");
23992
+ const text = readFileSync35(cfg.ship.logFile, "utf8").split("\n");
23549
23993
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
23550
23994
  `);
23551
23995
  } catch {
@@ -23568,10 +24012,10 @@ ${where.join("\n")}`);
23568
24012
  };
23569
24013
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
23570
24014
  const tracker = new Tracker(route2, cfg.ship);
23571
- const configText = readFileSync34(cfg.configFile, "utf8");
24015
+ const configText = readFileSync35(cfg.configFile, "utf8");
23572
24016
  const cfgBase = dirname22(cfg.configFile);
23573
- const configured = configuredRepos(configText, route2.route, cfgBase);
23574
- const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
24017
+ const configured2 = configuredRepos(configText, route2.route, cfgBase);
24018
+ const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
23575
24019
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
23576
24020
  const rows = await tracker.repoRows();
23577
24021
  const row = rows.find((r) => r.name === SAMPLE_REPO);
@@ -23595,9 +24039,9 @@ ${where.join("\n")}`);
23595
24039
  say("dry-run", { dir, row: row?.id ?? null, closed }, `would close ${closed.join(", ") || "no tickets"}, delete the Repos row, detach and delete ${dir}`);
23596
24040
  break;
23597
24041
  }
23598
- if (configured.some((r) => r.dir === dir)) {
24042
+ if (configured2.some((r) => r.dir === dir)) {
23599
24043
  try {
23600
- writeFileSync26(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
24044
+ writeFileSync27(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
23601
24045
  } catch (e) {
23602
24046
  if (e instanceof ReposError) fail(e.message);
23603
24047
  throw e;
@@ -23605,29 +24049,29 @@ ${where.join("\n")}`);
23605
24049
  }
23606
24050
  if (row) {
23607
24051
  await tracker.deleteRepoRow(row.id);
23608
- if (existsSync39(resolvedFile)) {
23609
- const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
24052
+ if (existsSync40(resolvedFile)) {
24053
+ const raw = JSON.parse(readFileSync35(resolvedFile, "utf8"));
23610
24054
  if (raw.repoNames) delete raw.repoNames[row.id];
23611
24055
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
23612
- writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
24056
+ writeFileSync27(resolvedFile, `${JSON.stringify(raw, null, 2)}
23613
24057
  `);
23614
24058
  }
23615
24059
  }
23616
- if (existsSync39(join43(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync18(dir, { recursive: true, force: true });
24060
+ if (existsSync40(join44(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync19(dir, { recursive: true, force: true });
23617
24061
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
23618
24062
  break;
23619
24063
  }
23620
24064
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
23621
- const fresh = !existsSync39(join43(dir, ".git"));
23622
- if (existsSync39(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
24065
+ const fresh = !existsSync40(join44(dir, ".git"));
24066
+ if (existsSync40(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
23623
24067
  if (dryRun) {
23624
24068
  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`);
23625
24069
  break;
23626
24070
  }
23627
24071
  if (fresh) {
23628
- mkdirSync25(dir, { recursive: true });
23629
- writeFileSync26(join43(dir, "README.md"), SAMPLE_README);
23630
- writeFileSync26(join43(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
24072
+ mkdirSync26(dir, { recursive: true });
24073
+ writeFileSync27(join44(dir, "README.md"), SAMPLE_README);
24074
+ writeFileSync27(join44(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
23631
24075
  const g = (...a) => {
23632
24076
  const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
23633
24077
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -23645,17 +24089,17 @@ ${where.join("\n")}`);
23645
24089
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
23646
24090
  });
23647
24091
  repoId = (await tracker.createRepoRow(payload)).id;
23648
- if (existsSync39(resolvedFile)) {
23649
- const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
24092
+ if (existsSync40(resolvedFile)) {
24093
+ const raw = JSON.parse(readFileSync35(resolvedFile, "utf8"));
23650
24094
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
23651
24095
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
23652
- writeFileSync26(resolvedFile, `${JSON.stringify(raw, null, 2)}
24096
+ writeFileSync27(resolvedFile, `${JSON.stringify(raw, null, 2)}
23653
24097
  `);
23654
24098
  }
23655
24099
  }
23656
- if (!configured.some((r) => r.dir === dir)) {
24100
+ if (!configured2.some((r) => r.dir === dir)) {
23657
24101
  try {
23658
- writeFileSync26(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
24102
+ writeFileSync27(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
23659
24103
  } catch (e) {
23660
24104
  if (e instanceof ReposError) fail(e.message);
23661
24105
  throw e;
@@ -23990,7 +24434,7 @@ watermark: ${state.watermark()}
23990
24434
  }
23991
24435
  for (const r of repos) {
23992
24436
  const remote = r.config.branch.remote;
23993
- if (existsSync39(r.dir)) {
24437
+ if (existsSync40(r.dir)) {
23994
24438
  pushAuth.push({
23995
24439
  route: c.route,
23996
24440
  repo: r.name,
@@ -24063,6 +24507,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
24063
24507
  for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
24064
24508
  `);
24065
24509
  process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
24510
+ `);
24511
+ process.stdout.write(`key store: ${describeKeyStore(cfg.ship)}
24066
24512
  `);
24067
24513
  const previewProblem = await previewHostProblem(cfg);
24068
24514
  if (previewProblem) {
@@ -24073,7 +24519,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
24073
24519
  }
24074
24520
  try {
24075
24521
  const evFile = eventFileFor(cfg.ship.stateDir);
24076
- const evSize = existsSync39(evFile) ? statSync13(evFile).size : 0;
24522
+ const evSize = existsSync40(evFile) ? statSync14(evFile).size : 0;
24077
24523
  process.stdout.write(
24078
24524
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
24079
24525
  ` : "events log: ok\n"
@@ -24084,7 +24530,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
24084
24530
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
24085
24531
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
24086
24532
  try {
24087
- return readFileSync34(p, "utf8");
24533
+ return readFileSync35(p, "utf8");
24088
24534
  } catch {
24089
24535
  return null;
24090
24536
  }
@@ -24100,7 +24546,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
24100
24546
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
24101
24547
  if (changed2) {
24102
24548
  process.stdout.write(
24103
- `daemon config: WARN ${changed2} changed at ${new Date(statSync13(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
24549
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync14(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
24104
24550
  `
24105
24551
  );
24106
24552
  }