@tablation/crew 0.106.0 → 0.107.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 +123 -81
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2185,6 +2185,12 @@ function parseSandbox(raw, file) {
2185
2185
  const r = raw;
2186
2186
  const packages = stringList(r.packages, "sandbox.packages", file);
2187
2187
  const setup = stringList(r.setup, "sandbox.setup", file);
2188
+ const warm = stringList(r.warm, "sandbox.warm", file);
2189
+ for (const w of warm) {
2190
+ if (w.startsWith("/") || w.split("/").some((seg) => seg === ".." || seg === "" || seg === ".") || /[\\\s]/.test(w)) {
2191
+ throw new RepoConfigError(`${file}: sandbox.warm entry "${w}" must be a repo-relative path or glob with no "..", empty segments, backslashes or whitespace`);
2192
+ }
2193
+ }
2188
2194
  for (const p of packages) {
2189
2195
  if (!PACKAGE_NAME.test(p)) throw new RepoConfigError(`${file}: sandbox.packages entry "${p}" is not a package name (lowercase letters, digits, + . - and an optional =version)`);
2190
2196
  }
@@ -2198,7 +2204,7 @@ function parseSandbox(raw, file) {
2198
2204
  throw new RepoConfigError(`${file}: sandbox.image and sandbox.packages are alternatives \u2014 a published image already carries its packages; put the rest in sandbox.setup`);
2199
2205
  }
2200
2206
  }
2201
- return { ...image ? { image } : {}, packages, setup };
2207
+ return { ...image ? { image } : {}, packages, setup, warm };
2202
2208
  }
2203
2209
  function parseRepoConfig(text2, file) {
2204
2210
  let raw;
@@ -2218,7 +2224,7 @@ function parseRepoConfig(text2, file) {
2218
2224
  rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
2219
2225
  rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
2220
2226
  rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
2221
- rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup"]), "sandbox", file);
2227
+ rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup", "warm"]), "sandbox", file);
2222
2228
  if (raw.version === void 0) throw new RepoConfigError(`${file}: version is required`);
2223
2229
  if (raw.version !== SPEC_VERSION) {
2224
2230
  throw new RepoConfigError(
@@ -5367,8 +5373,9 @@ function attentionProblems(items, already) {
5367
5373
  // src/sandbox-image.ts
5368
5374
  import { createHash as createHash5 } from "crypto";
5369
5375
  import { spawn as spawn2 } from "child_process";
5376
+ import { execFileSync as execFileSync4 } from "child_process";
5370
5377
  import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync3, writeFileSync as writeFileSync10, rmSync as rmSync9 } from "fs";
5371
- import { join as join15 } from "path";
5378
+ import { dirname as dirname7, join as join15 } from "path";
5372
5379
  import { tmpdir as tmpdir2 } from "os";
5373
5380
 
5374
5381
  // src/passenger-containers.ts
@@ -5628,7 +5635,25 @@ function crewLayer(base) {
5628
5635
  ].join("\n");
5629
5636
  return { name: "crew", tag: `crew-sandbox-crew:${hash({ base: base.tag, dockerfile, files })}`, dockerfile, files };
5630
5637
  }
5631
- function repoLayer(repo, decl, crew) {
5638
+ var WARM_DIR = "/tmp/crew-warm";
5639
+ var WARM_MAX_BYTES = 64 * 1024 * 1024;
5640
+ var globToRegExp = (glob) => new RegExp(`^${glob.split("*").map((p) => p.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")}$`);
5641
+ function readWarmFiles(dir, ref, patterns) {
5642
+ if (patterns.length === 0) return {};
5643
+ const git2 = (args) => execFileSync4("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: WARM_MAX_BYTES, stdio: ["ignore", "pipe", "pipe"] });
5644
+ const all = git2(["ls-tree", "-r", "--name-only", ref]).split("\n").filter(Boolean);
5645
+ const res = patterns.map(globToRegExp);
5646
+ const out = {};
5647
+ let total = 0;
5648
+ for (const path of all.filter((f) => res.some((re) => re.test(f))).sort()) {
5649
+ const content = git2(["show", `${ref}:${path}`]);
5650
+ total += content.length;
5651
+ if (total > WARM_MAX_BYTES) throw new Error(`sandbox.warm files exceed ${WARM_MAX_BYTES} bytes`);
5652
+ out[path] = content;
5653
+ }
5654
+ return out;
5655
+ }
5656
+ function repoLayer(repo, decl, crew, warm) {
5632
5657
  const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
5633
5658
  const lines2 = [];
5634
5659
  if (decl.image) {
@@ -5650,15 +5675,22 @@ function repoLayer(repo, decl, crew) {
5650
5675
  `USER ${SANDBOX_UID}`,
5651
5676
  "WORKDIR /sandbox"
5652
5677
  );
5678
+ const files = {};
5679
+ const warming = Object.keys(warm).length > 0;
5680
+ if (warming) {
5681
+ for (const [path, content] of Object.entries(warm)) files[`warm/${path}`] = content;
5682
+ lines2.push(`COPY --chown=${SANDBOX_UID}:${SANDBOX_UID} warm/ ${WARM_DIR}/`, `WORKDIR ${WARM_DIR}`);
5683
+ }
5653
5684
  for (const step of decl.setup) lines2.push(run(step));
5685
+ if (warming) lines2.push("WORKDIR /sandbox", run(`rm -rf ${WARM_DIR}`));
5654
5686
  lines2.push(`LABEL io.crew.sandbox.layer="repo" io.crew.sandbox.repo=${JSON.stringify(repo)}`, "");
5655
5687
  const dockerfile = lines2.join("\n");
5656
- return { name: "repo", tag: `crew-sandbox/${slug(repo)}:${hash({ crew: crew.tag, dockerfile })}`, dockerfile, files: {} };
5688
+ return { name: "repo", tag: `crew-sandbox/${slug(repo)}:${hash({ crew: crew.tag, dockerfile, files })}`, dockerfile, files };
5657
5689
  }
5658
- function planSandbox(repo, decl) {
5690
+ function planSandbox(repo, decl, warm = {}) {
5659
5691
  const base = baseLayer();
5660
5692
  const crew = crewLayer(base);
5661
- const layer = repoLayer(repo, decl, crew);
5693
+ const layer = repoLayer(repo, decl, crew, warm);
5662
5694
  return { repo, tag: layer.tag, layers: [base, crew, layer] };
5663
5695
  }
5664
5696
  var sandboxStatePath = (stateDir) => join15(stateDir, "sandbox-images.json");
@@ -5733,7 +5765,10 @@ async function buildSandboxImage(plan, deps) {
5733
5765
  mkdirSync9(ctx, { recursive: true });
5734
5766
  try {
5735
5767
  writeFileSync10(join15(ctx, "Dockerfile"), layer.dockerfile);
5736
- for (const [name, content] of Object.entries(layer.files)) writeFileSync10(join15(ctx, name), content);
5768
+ for (const [name, content] of Object.entries(layer.files)) {
5769
+ mkdirSync9(dirname7(join15(ctx, name)), { recursive: true });
5770
+ writeFileSync10(join15(ctx, name), content);
5771
+ }
5737
5772
  const code = await deps.run(["build", "--tag", layer.tag, "--file", join15(ctx, "Dockerfile"), ctx], logPath);
5738
5773
  if (code !== 0) {
5739
5774
  state[plan.repo] = { ...entry, failure: { tag: plan.tag, at: now().toISOString(), logPath } };
@@ -5764,7 +5799,7 @@ function sandboxFailedItem(repo, tag, layer, logPath, nowIso = (/* @__PURE__ */
5764
5799
  async function sandboxBuildTick(repos, deps, force = false) {
5765
5800
  const nowMs = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
5766
5801
  for (const r of repos) {
5767
- const plan = planSandbox(r.name, r.decl);
5802
+ const plan = planSandbox(r.name, r.decl, r.warm);
5768
5803
  const state = readSandboxState(deps.stateDir)[r.name];
5769
5804
  const upToDate = state?.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag);
5770
5805
  if (upToDate) {
@@ -5787,10 +5822,10 @@ async function sandboxBuildTick(repos, deps, force = false) {
5787
5822
  }
5788
5823
 
5789
5824
  // src/doctor-checks.ts
5790
- import { execFileSync as execFileSync4 } from "child_process";
5825
+ import { execFileSync as execFileSync5 } from "child_process";
5791
5826
  var PROBE_BRANCH = "crew-probe-branch";
5792
5827
  var realGit = (cwd, args, env) => {
5793
- execFileSync4("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5828
+ execFileSync5("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5794
5829
  };
5795
5830
  function isSshRemote(url) {
5796
5831
  return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
@@ -5921,7 +5956,7 @@ import { spawnSync as spawnSync6 } from "child_process";
5921
5956
  import { readFileSync as readFileSync21, existsSync as existsSync24, statSync as statSync7, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5922
5957
  import { StringDecoder } from "string_decoder";
5923
5958
  import { finished } from "stream/promises";
5924
- import { join as join26, dirname as dirname8, basename as basename8 } from "path";
5959
+ import { join as join26, dirname as dirname9, basename as basename8 } from "path";
5925
5960
  import { createHash as createHash6 } from "crypto";
5926
5961
 
5927
5962
  // src/state.ts
@@ -8531,15 +8566,15 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
8531
8566
 
8532
8567
  // src/run-worktree.ts
8533
8568
  import { existsSync as existsSync17 } from "fs";
8534
- import { dirname as dirname7, join as join19 } from "path";
8569
+ import { dirname as dirname8, join as join19 } from "path";
8535
8570
 
8536
8571
  // src/ports.ts
8537
- import { execFileSync as execFileSync5 } from "child_process";
8572
+ import { execFileSync as execFileSync6 } from "child_process";
8538
8573
  import { existsSync as existsSync16 } from "fs";
8539
8574
  import { join as join18 } from "path";
8540
8575
  var run2 = (cmd, args) => {
8541
8576
  try {
8542
- return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8577
+ return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8543
8578
  } catch {
8544
8579
  return "";
8545
8580
  }
@@ -8680,7 +8715,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
8680
8715
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
8681
8716
  return { action: "recut", path: existing.path, branch, base: baseRef };
8682
8717
  }
8683
- const path = join19(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8718
+ const path = join19(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
8684
8719
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
8685
8720
  return { action: "cut-remote", path, branch: located.branch };
8686
8721
  }
@@ -9951,7 +9986,7 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
9951
9986
  }
9952
9987
 
9953
9988
  // src/runner.ts
9954
- import { spawn as spawn3, execFile as execFile2, execFileSync as execFileSync6 } from "child_process";
9989
+ import { spawn as spawn3, execFile as execFile2, execFileSync as execFileSync7 } from "child_process";
9955
9990
  import { mkdtempSync, writeFileSync as writeFileSync14, rmSync as rmSync13 } from "fs";
9956
9991
  import { tmpdir as tmpdir3 } from "os";
9957
9992
  import { basename as basename7, join as join25 } from "path";
@@ -10932,7 +10967,7 @@ function describeExecError(e) {
10932
10967
  }
10933
10968
  function gitOrigin(cwd) {
10934
10969
  try {
10935
- return execFileSync6("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
10970
+ return execFileSync7("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
10936
10971
  } catch {
10937
10972
  return null;
10938
10973
  }
@@ -10943,7 +10978,7 @@ function runnerFor(kind, sandbox) {
10943
10978
  function runnerOfSidecar(sc) {
10944
10979
  return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
10945
10980
  }
10946
- function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync6(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
10981
+ function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync7(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
10947
10982
  const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
10948
10983
  let version;
10949
10984
  try {
@@ -11219,7 +11254,7 @@ async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMi
11219
11254
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
11220
11255
  function openSink(path) {
11221
11256
  try {
11222
- mkdirSync13(dirname8(path), { recursive: true });
11257
+ mkdirSync13(dirname9(path), { recursive: true });
11223
11258
  return createWriteStream(path, { flags: "a" });
11224
11259
  } catch {
11225
11260
  return void 0;
@@ -11561,7 +11596,7 @@ function spawnAgent(plan, emit, hintMoved) {
11561
11596
  const id = identityOf(plan, adapter.harness);
11562
11597
  let streamFd;
11563
11598
  try {
11564
- mkdirSync13(dirname8(plan.streamPath), { recursive: true });
11599
+ mkdirSync13(dirname9(plan.streamPath), { recursive: true });
11565
11600
  streamFd = openSync3(plan.streamPath, "a");
11566
11601
  } catch (e) {
11567
11602
  void endSink(eventsSink);
@@ -11933,7 +11968,7 @@ function doctorSelftestLines(stored, toolchains) {
11933
11968
  }
11934
11969
 
11935
11970
  // src/cli.ts
11936
- import { resolve as resolve6, dirname as dirname25, join as join49, basename as basename15 } from "path";
11971
+ import { resolve as resolve6, dirname as dirname26, join as join49, basename as basename15 } from "path";
11937
11972
  import { fileURLToPath } from "url";
11938
11973
  import { hostname as hostname4, homedir as homedir11 } from "os";
11939
11974
  import { spawnSync as spawnSync10 } from "child_process";
@@ -12131,7 +12166,7 @@ function rosterMarkdown(roster, meId) {
12131
12166
 
12132
12167
  // src/connect-resume.ts
12133
12168
  import { mkdirSync as mkdirSync15, readFileSync as readFileSync23, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
12134
- import { dirname as dirname9, join as join28 } from "path";
12169
+ import { dirname as dirname10, join as join28 } from "path";
12135
12170
  function lastSessionFile(stateDir) {
12136
12171
  return join28(stateDir, "connect-session.json");
12137
12172
  }
@@ -12151,7 +12186,7 @@ function readPointers(stateDir) {
12151
12186
  }
12152
12187
  function writePointers(stateDir, pointers) {
12153
12188
  const file = lastSessionFile(stateDir);
12154
- mkdirSync15(dirname9(file), { recursive: true });
12189
+ mkdirSync15(dirname10(file), { recursive: true });
12155
12190
  writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
12156
12191
  `, { mode: 384 });
12157
12192
  }
@@ -12238,7 +12273,7 @@ async function signOutSessions(o) {
12238
12273
 
12239
12274
  // src/reset.ts
12240
12275
  import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
12241
- import { dirname as dirname10, join as join29 } from "path";
12276
+ import { dirname as dirname11, join as join29 } from "path";
12242
12277
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
12243
12278
  function clonedCheckoutsFile(stateDir) {
12244
12279
  return join29(stateDir, "cloned-checkouts.json");
@@ -12255,7 +12290,7 @@ function recordClonedCheckout(stateDir, dir) {
12255
12290
  const known = readClonedCheckouts(stateDir);
12256
12291
  if (known.includes(dir)) return;
12257
12292
  const file = clonedCheckoutsFile(stateDir);
12258
- mkdirSync16(dirname10(file), { recursive: true });
12293
+ mkdirSync16(dirname11(file), { recursive: true });
12259
12294
  writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
12260
12295
  `);
12261
12296
  }
@@ -12438,7 +12473,7 @@ async function runDeprovision(o, fx, deps) {
12438
12473
  // src/pool-member.ts
12439
12474
  import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
12440
12475
  import { homedir as homedir8 } from "os";
12441
- import { dirname as dirname11, join as join30 } from "path";
12476
+ import { dirname as dirname12, join as join30 } from "path";
12442
12477
  var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
12443
12478
  function backoffMs(attempt) {
12444
12479
  return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
@@ -12640,7 +12675,7 @@ function fileMemberStore(dir) {
12640
12675
  }
12641
12676
  },
12642
12677
  write(f) {
12643
- mkdirSync17(dirname11(file), { recursive: true, mode: 448 });
12678
+ mkdirSync17(dirname12(file), { recursive: true, mode: 448 });
12644
12679
  const tmp = `${file}.tmp`;
12645
12680
  writeFileSync19(tmp, `${JSON.stringify(f)}
12646
12681
  `, { mode: 384 });
@@ -12698,10 +12733,10 @@ import { existsSync as existsSync30 } from "fs";
12698
12733
  import { hostname as hostname3 } from "os";
12699
12734
 
12700
12735
  // src/machine-id.ts
12701
- import { execFileSync as execFileSync7 } from "child_process";
12736
+ import { execFileSync as execFileSync8 } from "child_process";
12702
12737
  import { createHash as createHash7 } from "crypto";
12703
12738
  import { existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
12704
- import { dirname as dirname12, join as join31 } from "path";
12739
+ import { dirname as dirname13, join as join31 } from "path";
12705
12740
  function machineIdPathFor(stateDir) {
12706
12741
  return join31(stateDir, "machine-id");
12707
12742
  }
@@ -12709,7 +12744,7 @@ var sha256 = (s) => createHash7("sha256").update(s).digest("hex");
12709
12744
  function readRawMachineId(platform2 = process.platform) {
12710
12745
  try {
12711
12746
  if (platform2 === "darwin") {
12712
- const out = execFileSync7("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
12747
+ const out = execFileSync8("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
12713
12748
  encoding: "utf8",
12714
12749
  stdio: ["ignore", "pipe", "ignore"]
12715
12750
  });
@@ -12735,7 +12770,7 @@ function ensureMachineId(stateDir, opts = {}) {
12735
12770
  const raw = (opts.read ?? readRawMachineId)();
12736
12771
  if (!raw) return void 0;
12737
12772
  const hashed = sha256(raw);
12738
- mkdirSync18(dirname12(path), { recursive: true });
12773
+ mkdirSync18(dirname13(path), { recursive: true });
12739
12774
  writeFileSync20(path, `${hashed}
12740
12775
  `, { mode: 384 });
12741
12776
  return hashed;
@@ -14931,7 +14966,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
14931
14966
 
14932
14967
  // src/repo-refresh.ts
14933
14968
  import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
14934
- import { dirname as dirname14 } from "path";
14969
+ import { dirname as dirname15 } from "path";
14935
14970
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
14936
14971
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
14937
14972
  var lastAsked = /* @__PURE__ */ new Map();
@@ -14963,7 +14998,7 @@ function persistRepoRows(stateDir, routeName, rows) {
14963
14998
  if (!existsSync31(path)) return;
14964
14999
  const raw = JSON.parse(readFileSync29(path, "utf8"));
14965
15000
  applyRepoRows(raw, rows);
14966
- mkdirSync20(dirname14(path), { recursive: true });
15001
+ mkdirSync20(dirname15(path), { recursive: true });
14967
15002
  writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
14968
15003
  `);
14969
15004
  }
@@ -15414,7 +15449,7 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
15414
15449
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
15415
15450
 
15416
15451
  // src/connect-wizard.ts
15417
- import { delimiter, dirname as dirname15, join as join35 } from "path";
15452
+ import { delimiter, dirname as dirname16, join as join35 } from "path";
15418
15453
  import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync25 } from "fs";
15419
15454
  import { stringify as stringify2 } from "yaml";
15420
15455
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
@@ -15463,9 +15498,9 @@ async function runWizard(p, env) {
15463
15498
  if (entered && isExec(entered)) agentBin = entered;
15464
15499
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
15465
15500
  }
15466
- const nodeDir = dirname15(env.nodePath);
15501
+ const nodeDir = dirname16(env.nodePath);
15467
15502
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
15468
- const baseDirs = [nodeDir, pnpmPath ? dirname15(pnpmPath) : void 0];
15503
+ const baseDirs = [nodeDir, pnpmPath ? dirname16(pnpmPath) : void 0];
15469
15504
  p.say(
15470
15505
  `
15471
15506
  Host Passengers lets this machine run the agent sessions that other people's
@@ -15476,7 +15511,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
15476
15511
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
15477
15512
  }
15478
15513
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
15479
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname15(dockerPath) : void 0]);
15514
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname16(dockerPath) : void 0]);
15480
15515
  if (extraPath) {
15481
15516
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
15482
15517
  }
@@ -15498,7 +15533,7 @@ routes:
15498
15533
  ${routeBlock}`;
15499
15534
  }
15500
15535
  function writeNewConfig(path, text2) {
15501
- mkdirSync21(dirname15(path), { recursive: true });
15536
+ mkdirSync21(dirname16(path), { recursive: true });
15502
15537
  try {
15503
15538
  writeFileSync25(path, text2, { mode: 384, flag: "wx" });
15504
15539
  return true;
@@ -15534,7 +15569,7 @@ Wrote ${o.configPath}.
15534
15569
  // src/connect-json.ts
15535
15570
  import { accessSync as accessSync4, constants as constants4 } from "fs";
15536
15571
  import { homedir as homedir10 } from "os";
15537
- import { dirname as dirname16, join as join36 } from "path";
15572
+ import { dirname as dirname17, join as join36 } from "path";
15538
15573
  import { delimiter as delimiter2 } from "path";
15539
15574
  var CHOICE_EXIT_CODE = 3;
15540
15575
  function createEmitter(json2, write) {
@@ -15600,7 +15635,7 @@ function flagWizardAnswers(f, env) {
15600
15635
  const agentBin = f.agentBin ?? found?.bin;
15601
15636
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
15602
15637
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
15603
- const extraPath = f.extraPath ?? deriveExtraPath([dirname16(env.nodePath), pnpmPath ? dirname16(pnpmPath) : void 0, dockerPath ? dirname16(dockerPath) : void 0]);
15638
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname17(env.nodePath), pnpmPath ? dirname17(pnpmPath) : void 0, dockerPath ? dirname17(dockerPath) : void 0]);
15604
15639
  return {
15605
15640
  answers: {
15606
15641
  name: f.shipName?.trim() || env.hostname,
@@ -15615,7 +15650,7 @@ function flagWizardAnswers(f, env) {
15615
15650
 
15616
15651
  // src/release-run.ts
15617
15652
  import { readFileSync as readFileSync30, writeFileSync as writeFileSync26, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, rmSync as rmSync17 } from "fs";
15618
- import { dirname as dirname17, join as join37 } from "path";
15653
+ import { dirname as dirname18, join as join37 } from "path";
15619
15654
  import { tmpdir as tmpdir5 } from "os";
15620
15655
 
15621
15656
  // src/release.ts
@@ -16501,7 +16536,7 @@ function openReleaseCheckout(o) {
16501
16536
  for (const f of o.repo.worktrees.copy) {
16502
16537
  const src = join37(o.cwd, f);
16503
16538
  if (!existsSync33(src)) continue;
16504
- mkdirSync22(dirname17(join37(dir, f)), { recursive: true });
16539
+ mkdirSync22(dirname18(join37(dir, f)), { recursive: true });
16505
16540
  copyFileSync(src, join37(dir, f));
16506
16541
  }
16507
16542
  return { dir, branch, close };
@@ -17821,7 +17856,7 @@ function nextRoles(decision, ran) {
17821
17856
 
17822
17857
  // src/ship-name.ts
17823
17858
  import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
17824
- import { dirname as dirname18 } from "path";
17859
+ import { dirname as dirname19 } from "path";
17825
17860
  import { parseDocument as parseDocument5 } from "yaml";
17826
17861
  var ShipNameError = class extends Error {
17827
17862
  };
@@ -17900,7 +17935,7 @@ function writeSyncedName(resolvedFile, name) {
17900
17935
  }
17901
17936
  if (raw.shipName === name) return;
17902
17937
  raw.shipName = name;
17903
- mkdirSync23(dirname18(resolvedFile), { recursive: true });
17938
+ mkdirSync23(dirname19(resolvedFile), { recursive: true });
17904
17939
  const tmp = `${resolvedFile}.tmp${process.pid}`;
17905
17940
  writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
17906
17941
  `);
@@ -18594,7 +18629,7 @@ function describeCapability(i) {
18594
18629
 
18595
18630
  // src/agents-sync-run.ts
18596
18631
  import { existsSync as existsSync36, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
18597
- import { dirname as dirname19 } from "path";
18632
+ import { dirname as dirname20 } from "path";
18598
18633
  async function runAgentsSync(target, o) {
18599
18634
  const dryRun = o.dryRun ?? false;
18600
18635
  const force = o.force ?? false;
@@ -18631,7 +18666,7 @@ async function runAgentsSync(target, o) {
18631
18666
  raw.agentPersonas = personaResult.agentPersonas;
18632
18667
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
18633
18668
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
18634
- mkdirSync24(dirname19(resolvedPath), { recursive: true });
18669
+ mkdirSync24(dirname20(resolvedPath), { recursive: true });
18635
18670
  writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
18636
18671
  `);
18637
18672
  if (target.resolved) {
@@ -19052,7 +19087,7 @@ function existingRepoConfig(f) {
19052
19087
  }
19053
19088
 
19054
19089
  // src/repos-hooks-agent.ts
19055
- import { spawn as spawn7, execFileSync as execFileSync8 } from "child_process";
19090
+ import { spawn as spawn7, execFileSync as execFileSync9 } from "child_process";
19056
19091
  import { realpathSync as realpathSync2 } from "fs";
19057
19092
  import { isAbsolute as isAbsolute2, relative } from "path";
19058
19093
  var AgentSuggestError = class extends Error {
@@ -19155,7 +19190,7 @@ function classifyFailure(o) {
19155
19190
  }
19156
19191
  function porcelain(dir) {
19157
19192
  try {
19158
- return execFileSync8("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
19193
+ return execFileSync9("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
19159
19194
  } catch {
19160
19195
  return void 0;
19161
19196
  }
@@ -19224,7 +19259,7 @@ ${run3.err}` });
19224
19259
  }
19225
19260
 
19226
19261
  // src/sample-ticket.ts
19227
- import { basename as basename11, dirname as dirname20, join as join38 } from "path";
19262
+ import { basename as basename11, dirname as dirname21, join as join38 } from "path";
19228
19263
  var SAMPLE_REPO = "crew-sample";
19229
19264
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
19230
19265
  var SAMPLE_MARKER = "sample: true";
@@ -19247,7 +19282,7 @@ has something safe to work on: its test and build hooks always pass, it has no
19247
19282
  remote, and \`crew sample-ticket --remove\` deletes it.
19248
19283
  `;
19249
19284
  function sampleDir(existing, reposBase, workspace) {
19250
- if (existing.length > 0) return join38(dirname20(existing[0]), SAMPLE_REPO);
19285
+ if (existing.length > 0) return join38(dirname21(existing[0]), SAMPLE_REPO);
19251
19286
  return join38(reposBase, workspace, SAMPLE_REPO);
19252
19287
  }
19253
19288
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
@@ -19293,7 +19328,7 @@ function planSampleTicket(i) {
19293
19328
 
19294
19329
  // src/worktree-sweep.ts
19295
19330
  import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync14 } from "fs";
19296
- import { basename as basename12, dirname as dirname21, join as join39, resolve as resolve4 } from "path";
19331
+ import { basename as basename12, dirname as dirname22, join as join39, resolve as resolve4 } from "path";
19297
19332
  function planWorktreeSweep(target, tickets, contract) {
19298
19333
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
19299
19334
  const verified = contract.statuses.verified;
@@ -19357,7 +19392,7 @@ function worktreeTicketNumbers(target) {
19357
19392
  return [...nums];
19358
19393
  }
19359
19394
  function siblingWorktreeDirs(target) {
19360
- const parent = dirname21(target.dir.replace(/[/\\]+$/, ""));
19395
+ const parent = dirname22(target.dir.replace(/[/\\]+$/, ""));
19361
19396
  const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
19362
19397
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
19363
19398
  let names;
@@ -20050,14 +20085,14 @@ ${tail2(preview.output)}`);
20050
20085
  }
20051
20086
 
20052
20087
  // src/periscope-idle.ts
20053
- import { execFileSync as execFileSync9 } from "child_process";
20088
+ import { execFileSync as execFileSync10 } from "child_process";
20054
20089
  import { statSync as statSync11 } from "fs";
20055
20090
  import { join as join42 } from "path";
20056
20091
  var MAX_SCANNED_FILES = 2e4;
20057
20092
  function newestWorktreeMtime(root) {
20058
20093
  let listing;
20059
20094
  try {
20060
- listing = execFileSync9("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
20095
+ listing = execFileSync10("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
20061
20096
  cwd: root,
20062
20097
  encoding: "utf8",
20063
20098
  stdio: ["ignore", "pipe", "ignore"],
@@ -20147,9 +20182,9 @@ async function runPeriscopeIdlePass(d) {
20147
20182
  import { dirname as dirOf2, resolve as resolvePath } from "path";
20148
20183
 
20149
20184
  // src/ssh-keys.ts
20150
- import { execFileSync as execFileSync10 } from "child_process";
20185
+ import { execFileSync as execFileSync11 } from "child_process";
20151
20186
  import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
20152
- import { dirname as dirname22, join as join43 } from "path";
20187
+ import { dirname as dirname23, join as join43 } from "path";
20153
20188
  var SshKeyError = class extends Error {
20154
20189
  };
20155
20190
  function shipSshKeyPathFor(stateDir) {
@@ -20159,9 +20194,9 @@ function ensureShipSshKeypair(stateDir) {
20159
20194
  const privateKeyPath = shipSshKeyPathFor(stateDir);
20160
20195
  const publicKeyPath = `${privateKeyPath}.pub`;
20161
20196
  if (!existsSync40(privateKeyPath)) {
20162
- mkdirSync26(dirname22(privateKeyPath), { recursive: true });
20197
+ mkdirSync26(dirname23(privateKeyPath), { recursive: true });
20163
20198
  try {
20164
- execFileSync10("ssh-keygen", [
20199
+ execFileSync11("ssh-keygen", [
20165
20200
  "-t",
20166
20201
  "ed25519",
20167
20202
  "-N",
@@ -20187,7 +20222,7 @@ function ensureShipSshKeypair(stateDir) {
20187
20222
  }
20188
20223
  function sshKeygenAvailable() {
20189
20224
  try {
20190
- execFileSync10("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
20225
+ execFileSync11("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
20191
20226
  return true;
20192
20227
  } catch (e) {
20193
20228
  return e.code !== "ENOENT";
@@ -21257,7 +21292,7 @@ import {
21257
21292
  } from "fs";
21258
21293
  import { createConnection } from "net";
21259
21294
  import { createServer } from "http";
21260
- import { basename as basename13, dirname as dirname23, join as join46 } from "path";
21295
+ import { basename as basename13, dirname as dirname24, join as join46 } from "path";
21261
21296
  import { execFile as execFile3, spawn as spawn10 } from "child_process";
21262
21297
 
21263
21298
  // src/periscope-control.ts
@@ -21573,7 +21608,7 @@ function candidates(live, rotated) {
21573
21608
  const out = [live];
21574
21609
  if (!rotated) return out;
21575
21610
  try {
21576
- for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname23(live), n));
21611
+ for (const n of readdirSync18(dirname24(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname24(live), n));
21577
21612
  } catch {
21578
21613
  }
21579
21614
  return out;
@@ -21733,7 +21768,7 @@ async function tailReply(path, q2, rotated, abort) {
21733
21768
  const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
21734
21769
  let r = readTail(path, { after, tail: tail3, rotated });
21735
21770
  while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
21736
- await waitForChange(dirname23(path), deadline - Date.now(), abort);
21771
+ await waitForChange(dirname24(path), deadline - Date.now(), abort);
21737
21772
  r = readTail(path, { after: r.cursor, rotated });
21738
21773
  }
21739
21774
  return reply2(200, r);
@@ -21923,7 +21958,7 @@ function crewJson(args, timeoutMs = 6e4) {
21923
21958
 
21924
21959
  // src/periscope-mcp.ts
21925
21960
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
21926
- import { basename as basename14, dirname as dirname24, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
21961
+ import { basename as basename14, dirname as dirname25, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
21927
21962
  import { z } from "zod";
21928
21963
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21929
21964
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -22061,10 +22096,10 @@ async function realAncestor(p) {
22061
22096
  }
22062
22097
  if (isLink) {
22063
22098
  if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
22064
- cur = resolve5(dirname24(cur), await readlink(cur));
22099
+ cur = resolve5(dirname25(cur), await readlink(cur));
22065
22100
  continue;
22066
22101
  }
22067
- const parent = dirname24(cur);
22102
+ const parent = dirname25(cur);
22068
22103
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
22069
22104
  tail3.push(basename14(cur));
22070
22105
  cur = parent;
@@ -22087,7 +22122,7 @@ async function resolveInWorktree(root, requested) {
22087
22122
  if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
22088
22123
  return final;
22089
22124
  }
22090
- function globToRegExp(glob) {
22125
+ function globToRegExp2(glob) {
22091
22126
  let re = "";
22092
22127
  for (let i = 0; i < glob.length; i++) {
22093
22128
  const c = glob[i];
@@ -22161,7 +22196,7 @@ function createPeriscopeMcpServer(d, sc) {
22161
22196
  inputSchema: { pattern: z.string() },
22162
22197
  annotations: { readOnlyHint: true, openWorldHint: false }
22163
22198
  }, async ({ pattern }) => {
22164
- const re = globToRegExp(pattern);
22199
+ const re = globToRegExp2(pattern);
22165
22200
  const listing = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
22166
22201
  const hits = listing.split("\0").filter((f) => f && re.test(f)).slice(0, MAX_SEARCH_RESULTS);
22167
22202
  return text(hits.length ? hits.join("\n") : "No matches found");
@@ -22175,7 +22210,7 @@ function createPeriscopeMcpServer(d, sc) {
22175
22210
  if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
22176
22211
  const abs = await inTree(path);
22177
22212
  if (abs === await realpath(root)) return text("path must name a file", true);
22178
- await mkdir(dirname24(abs), { recursive: true });
22213
+ await mkdir(dirname25(abs), { recursive: true });
22179
22214
  await inTree(path);
22180
22215
  await writeFile(abs, content);
22181
22216
  return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
@@ -22215,7 +22250,7 @@ function createPeriscopeMcpServer(d, sc) {
22215
22250
  inputSchema: { path: z.string() },
22216
22251
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
22217
22252
  }, async ({ path }) => {
22218
- const parent = await inTree(dirname24(path) === "" ? "." : dirname24(path));
22253
+ const parent = await inTree(dirname25(path) === "" ? "." : dirname25(path));
22219
22254
  const abs = join48(parent, basename14(path));
22220
22255
  if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
22221
22256
  const st = await lstat(abs).catch(() => void 0);
@@ -22337,7 +22372,7 @@ function findPeriscope(sidecars, segment) {
22337
22372
  }
22338
22373
 
22339
22374
  // src/cli.ts
22340
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname25(process.execPath) : resolve6(dirname25(fileURLToPath(import.meta.url)), "..");
22375
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname26(process.execPath) : resolve6(dirname26(fileURLToPath(import.meta.url)), "..");
22341
22376
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
22342
22377
  var CYCLE_SECONDS2 = 120;
22343
22378
  var STALE_CYCLES = 3;
@@ -23325,7 +23360,14 @@ ${changed2.map((c) => ` - ${c}
23325
23360
  const out = /* @__PURE__ */ new Map();
23326
23361
  for (const c of routes) {
23327
23362
  const { settled } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
23328
- for (const r of settled) if (r.config.sandbox && !out.has(r.name)) out.set(r.name, { name: r.name, decl: r.config.sandbox });
23363
+ for (const r of settled) if (r.config.sandbox && !out.has(r.name)) {
23364
+ let warm = {};
23365
+ try {
23366
+ warm = readWarmFiles(r.dir, `${r.config.branch.remote}/${r.config.branch.base}`, r.config.sandbox.warm ?? []);
23367
+ } catch {
23368
+ }
23369
+ out.set(r.name, { name: r.name, decl: r.config.sandbox, warm });
23370
+ }
23329
23371
  }
23330
23372
  return [...out.values()];
23331
23373
  }
@@ -24818,7 +24860,7 @@ ${cu.tail.trimEnd()}
24818
24860
  const found = cfg.routes.find((r) => r.route === routeArg);
24819
24861
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
24820
24862
  const named2 = value("route") ? void 0 : positional[3];
24821
- const dir = named2 ? repoDirOf(configText, dirname25(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
24863
+ const dir = named2 ? repoDirOf(configText, dirname26(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
24822
24864
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
24823
24865
  const o = target ? found.repoOverrides[target.name] : void 0;
24824
24866
  runReposCheck(dir, {
@@ -24841,7 +24883,7 @@ ${cu.tail.trimEnd()}
24841
24883
  if (sub === "hooks") {
24842
24884
  try {
24843
24885
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
24844
- const dir = repoDirOf(configText, dirname25(cfg.configFile), a.route, a.name);
24886
+ const dir = repoDirOf(configText, dirname26(cfg.configFile), a.route, a.name);
24845
24887
  if (!existsSync45(dir)) throw new ReposError(`${dir} does not exist on disk`);
24846
24888
  const files = { exists: (r) => existsSync45(resolvePath(dir, r)), list: (r) => {
24847
24889
  try {
@@ -25008,7 +25050,7 @@ ${cu.tail.trimEnd()}
25008
25050
  }
25009
25051
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
25010
25052
  const text2 = configText;
25011
- const base = dirname25(cfg.configFile);
25053
+ const base = dirname26(cfg.configFile);
25012
25054
  const isCheckout = (d) => existsSync45(`${d}/.git`);
25013
25055
  try {
25014
25056
  if (sub === "list") {
@@ -25684,7 +25726,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25684
25726
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
25685
25727
  `);
25686
25728
  } else {
25687
- mkdirSync30(dirname25(resolvedPath), { recursive: true });
25729
+ mkdirSync30(dirname26(resolvedPath), { recursive: true });
25688
25730
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
25689
25731
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
25690
25732
  writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -25733,7 +25775,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25733
25775
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
25734
25776
  `);
25735
25777
  } else {
25736
- mkdirSync30(dirname25(apiKeyFile), { recursive: true });
25778
+ mkdirSync30(dirname26(apiKeyFile), { recursive: true });
25737
25779
  writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
25738
25780
  `, { mode: 384 });
25739
25781
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -26304,7 +26346,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26304
26346
  if (subS === "images") {
26305
26347
  const stateS = readSandboxState(cfg.ship.stateDir);
26306
26348
  const rows = declared.map((r) => {
26307
- const plan = planSandbox(r.name, r.decl);
26349
+ const plan = planSandbox(r.name, r.decl, r.warm);
26308
26350
  return { repo: r.name, wants: plan.tag, tags: stateS[r.name]?.tags ?? [], ...sandboxImageStatus(stateS, plan) };
26309
26351
  });
26310
26352
  if (jsonS) {
@@ -26338,7 +26380,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26338
26380
  for (const t of targets) {
26339
26381
  await sandboxBuildTick([t], depsS, true);
26340
26382
  const after = readSandboxState(cfg.ship.stateDir);
26341
- const st = sandboxImageStatus(after, planSandbox(t.name, t.decl));
26383
+ const st = sandboxImageStatus(after, planSandbox(t.name, t.decl, t.warm));
26342
26384
  if (st.status === "failed") failed = true;
26343
26385
  process.stdout.write(`${t.name}: ${st.status} \u2014 ${st.detail}
26344
26386
  `);
@@ -27057,7 +27099,7 @@ ${where.join("\n")}`);
27057
27099
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
27058
27100
  const tracker = new Tracker(route2, cfg.ship);
27059
27101
  const configText = readFileSync40(cfg.configFile, "utf8");
27060
- const cfgBase = dirname25(cfg.configFile);
27102
+ const cfgBase = dirname26(cfg.configFile);
27061
27103
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
27062
27104
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
27063
27105
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
@@ -28076,7 +28118,7 @@ ${foreign.join("\n")}
28076
28118
  const declaredS = await sandboxReposOf(cfg.routes.filter((r) => r.enabled));
28077
28119
  const stateS = readSandboxState(cfg.ship.stateDir);
28078
28120
  for (const r of declaredS) {
28079
- const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl));
28121
+ const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl, r.warm));
28080
28122
  process.stdout.write(`sandbox image: ${r.name}: ${st.status.toUpperCase()} \u2014 ${st.detail}
28081
28123
  `);
28082
28124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.106.0",
3
+ "version": "0.107.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",