@tablation/crew 0.104.0 → 0.105.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 +1110 -754
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1912,8 +1912,8 @@ function configureSecretStore(c) {
1912
1912
  function keysDir(stateDir) {
1913
1913
  return join10(stateDir, "keys");
1914
1914
  }
1915
- function entryName(host, slug) {
1916
- return `${createHash2("sha256").update(`${host}\0${slug}`).digest("hex")}.json`;
1915
+ function entryName(host, slug2) {
1916
+ return `${createHash2("sha256").update(`${host}\0${slug2}`).digest("hex")}.json`;
1917
1917
  }
1918
1918
  var FileSessionStore = class {
1919
1919
  dir;
@@ -1924,23 +1924,23 @@ var FileSessionStore = class {
1924
1924
  mkdirSync6(this.dir, { recursive: true, mode: 448 });
1925
1925
  if ((statSync3(this.dir).mode & 63) !== 0) chmodSync2(this.dir, 448);
1926
1926
  }
1927
- async get(host, slug) {
1928
- const file = join10(this.dir, entryName(host, slug));
1927
+ async get(host, slug2) {
1928
+ const file = join10(this.dir, entryName(host, slug2));
1929
1929
  if (!existsSync8(this.dir)) return void 0;
1930
1930
  accessSync(this.dir, constants.R_OK | constants.X_OK);
1931
1931
  if (!existsSync8(file)) return void 0;
1932
1932
  return JSON.parse(readFileSync9(file, "utf8"));
1933
1933
  }
1934
- async set(host, slug, session) {
1934
+ async set(host, slug2, session) {
1935
1935
  this.ensureDir();
1936
- const file = join10(this.dir, entryName(host, slug));
1936
+ const file = join10(this.dir, entryName(host, slug2));
1937
1937
  const tmp = `${file}.${process.pid}.tmp`;
1938
1938
  writeFileSync7(tmp, JSON.stringify(session), { mode: 384 });
1939
1939
  renameSync2(tmp, file);
1940
1940
  }
1941
- async clear(host, slug) {
1941
+ async clear(host, slug2) {
1942
1942
  if (!existsSync8(this.dir)) return;
1943
- rmSync7(join10(this.dir, entryName(host, slug)), { force: true });
1943
+ rmSync7(join10(this.dir, entryName(host, slug2)), { force: true });
1944
1944
  }
1945
1945
  /** Whether a key could be written here right now, without leaving anything behind. */
1946
1946
  writable() {
@@ -1973,23 +1973,23 @@ var CrewHintedStore = class {
1973
1973
  constructor(inner) {
1974
1974
  this.inner = inner;
1975
1975
  }
1976
- async get(host, slug) {
1976
+ async get(host, slug2) {
1977
1977
  try {
1978
- return await this.inner.get(host, slug);
1978
+ return await this.inner.get(host, slug2);
1979
1979
  } catch (e) {
1980
1980
  throw crewifyStoreError(e);
1981
1981
  }
1982
1982
  }
1983
- async set(host, slug, s) {
1983
+ async set(host, slug2, s) {
1984
1984
  try {
1985
- await this.inner.set(host, slug, s);
1985
+ await this.inner.set(host, slug2, s);
1986
1986
  } catch (e) {
1987
1987
  throw crewifyStoreError(e);
1988
1988
  }
1989
1989
  }
1990
- async clear(host, slug) {
1990
+ async clear(host, slug2) {
1991
1991
  try {
1992
- await this.inner.clear(host, slug);
1992
+ await this.inner.clear(host, slug2);
1993
1993
  } catch (e) {
1994
1994
  throw crewifyStoreError(e);
1995
1995
  }
@@ -2102,7 +2102,8 @@ var TOP_LEVEL = /* @__PURE__ */ new Set([
2102
2102
  "docs",
2103
2103
  "hooks",
2104
2104
  "labels",
2105
- "release"
2105
+ "release",
2106
+ "sandbox"
2106
2107
  ]);
2107
2108
  var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote", "lowercase"]);
2108
2109
  var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag|issue)\}/g;
@@ -2169,6 +2170,36 @@ function rejectUnknown(obj, allowed, where, file) {
2169
2170
  );
2170
2171
  }
2171
2172
  }
2173
+ var PACKAGE_NAME = /^[a-z0-9][a-z0-9+.\-]*(=[A-Za-z0-9.+:~\-]+)?$/;
2174
+ function stringList(raw, where, file) {
2175
+ if (raw === void 0 || raw === null) return [];
2176
+ if (!Array.isArray(raw)) throw new RepoConfigError(`${file}: ${where} must be a list of strings`);
2177
+ return raw.map((v) => {
2178
+ if (typeof v !== "string" || v.trim() === "") throw new RepoConfigError(`${file}: ${where} entries must be non-empty strings`);
2179
+ return v;
2180
+ });
2181
+ }
2182
+ function parseSandbox(raw, file) {
2183
+ if (raw === void 0 || raw === null) return void 0;
2184
+ if (typeof raw !== "object" || Array.isArray(raw)) throw new RepoConfigError(`${file}: sandbox must be a mapping`);
2185
+ const r = raw;
2186
+ const packages = stringList(r.packages, "sandbox.packages", file);
2187
+ const setup = stringList(r.setup, "sandbox.setup", file);
2188
+ for (const p of packages) {
2189
+ 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
+ }
2191
+ let image;
2192
+ if (r.image !== void 0 && r.image !== null) {
2193
+ if (typeof r.image !== "string" || r.image.trim() === "" || /\s/.test(r.image)) {
2194
+ throw new RepoConfigError(`${file}: sandbox.image must be an image reference with no whitespace`);
2195
+ }
2196
+ image = r.image;
2197
+ if (packages.length > 0) {
2198
+ 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
+ }
2200
+ }
2201
+ return { ...image ? { image } : {}, packages, setup };
2202
+ }
2172
2203
  function parseRepoConfig(text2, file) {
2173
2204
  let raw;
2174
2205
  try {
@@ -2187,6 +2218,7 @@ function parseRepoConfig(text2, file) {
2187
2218
  rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
2188
2219
  rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
2189
2220
  rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
2221
+ rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup"]), "sandbox", file);
2190
2222
  if (raw.version === void 0) throw new RepoConfigError(`${file}: version is required`);
2191
2223
  if (raw.version !== SPEC_VERSION) {
2192
2224
  throw new RepoConfigError(
@@ -2243,6 +2275,7 @@ function parseRepoConfig(text2, file) {
2243
2275
  );
2244
2276
  }
2245
2277
  }
2278
+ const sandbox = parseSandbox(raw.sandbox, file);
2246
2279
  const platform2 = String(raw.platform ?? "any");
2247
2280
  if (!isPlatformRequirement(platform2)) {
2248
2281
  throw new RepoConfigError(
@@ -2371,6 +2404,7 @@ function parseRepoConfig(text2, file) {
2371
2404
  branch,
2372
2405
  worktrees: worktrees2,
2373
2406
  docs,
2407
+ ...sandbox ? { sandbox } : {},
2374
2408
  platform: platform2,
2375
2409
  shell: raw.shell ? String(raw.shell) : void 0,
2376
2410
  hooks,
@@ -2489,6 +2523,8 @@ function resolveRepoConfig(repo, ship, dir) {
2489
2523
  );
2490
2524
  return {
2491
2525
  version: SPEC_VERSION,
2526
+ // Repo-only, like platform: what a repo needs installed is a fact about it.
2527
+ ...repo?.sandbox ? { sandbox: repo.sandbox } : {},
2492
2528
  worktrees: {
2493
2529
  // Not merged: which gitignored files a worktree needs is a fact about
2494
2530
  // the repository and nothing else, so a ship cannot supply or override
@@ -3864,9 +3900,9 @@ function labelFor(crewHome, job) {
3864
3900
  return crewHome;
3865
3901
  }
3866
3902
  })();
3867
- const hash = createHash3("sha1").update(real).digest("hex").slice(0, 8);
3903
+ const hash2 = createHash3("sha1").update(real).digest("hex").slice(0, 8);
3868
3904
  const stem = job === "release" ? "com.tablation.crew-release" : job === "passengers" ? "com.tablation.crew-passengers" : "com.tablation.crew";
3869
- return `${stem}.${hash}`;
3905
+ return `${stem}.${hash2}`;
3870
3906
  }
3871
3907
  function cronMarkerFor(label) {
3872
3908
  return `# crew:${label} \u2014 managed by \`crew install\`, do not edit by hand`;
@@ -5313,11 +5349,433 @@ function attentionProblems(items, already) {
5313
5349
  return items.filter((i) => !seen.has(i.key)).map((i) => ({ severity: "attention", kind: i.kind, message: i.message }));
5314
5350
  }
5315
5351
 
5316
- // src/doctor-checks.ts
5352
+ // src/sandbox-image.ts
5353
+ import { createHash as createHash5 } from "crypto";
5354
+ import { spawn as spawn2 } from "child_process";
5355
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync3, writeFileSync as writeFileSync10, rmSync as rmSync9 } from "fs";
5356
+ import { join as join15 } from "path";
5357
+ import { tmpdir as tmpdir2 } from "os";
5358
+
5359
+ // src/passenger-containers.ts
5317
5360
  import { execFileSync as execFileSync3 } from "child_process";
5361
+ import { createHash as createHash4 } from "crypto";
5362
+ var realExec = (cmd, args, opts) => {
5363
+ try {
5364
+ return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
5365
+ } catch (e) {
5366
+ const err3 = e;
5367
+ const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
5368
+ throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
5369
+ }
5370
+ };
5371
+ var DOCKER_INFO_TIMEOUT_MS = 1e4;
5372
+ function dockerAvailable(exec = realExec) {
5373
+ try {
5374
+ exec("docker", ["info"], { timeoutMs: DOCKER_INFO_TIMEOUT_MS });
5375
+ return true;
5376
+ } catch {
5377
+ return false;
5378
+ }
5379
+ }
5380
+ var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
5381
+ function passengerImageRef(cfg) {
5382
+ return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
5383
+ }
5384
+ function imagePresent(image, exec = realExec) {
5385
+ try {
5386
+ exec("docker", ["image", "inspect", image]);
5387
+ return true;
5388
+ } catch {
5389
+ return false;
5390
+ }
5391
+ }
5392
+ function ensureImage(image, exec = realExec) {
5393
+ if (!imagePresent(image, exec)) exec("docker", ["pull", image]);
5394
+ }
5395
+ function containerNameFor(workspaceId) {
5396
+ return `crew-passenger-${workspaceId}`;
5397
+ }
5398
+ function portFor(workspaceId, base = 28765, span = 1e3) {
5399
+ const digest = createHash4("sha256").update(workspaceId).digest();
5400
+ return base + digest.readUInt32BE(0) % span;
5401
+ }
5402
+ function mountsDigest(mounts, image) {
5403
+ const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
5404
+ return createHash4("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
5405
+ }
5406
+ function planContainers(cfg) {
5407
+ const byWorkspace = /* @__PURE__ */ new Map();
5408
+ const image = passengerImageRef(cfg);
5409
+ for (const route2 of cfg.routes) {
5410
+ if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
5411
+ const workspaceId = route2.resolved.workspaceId;
5412
+ const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
5413
+ for (const target of passengerCheckoutTargets(route2)) {
5414
+ mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
5415
+ }
5416
+ byWorkspace.set(workspaceId, mounts);
5417
+ }
5418
+ return [...byWorkspace.entries()].filter(([, mounts]) => mounts.size > 0).map(([workspaceId, mounts]) => {
5419
+ const mountList = [...mounts.values()];
5420
+ return {
5421
+ workspaceId,
5422
+ containerName: containerNameFor(workspaceId),
5423
+ port: portFor(workspaceId),
5424
+ image,
5425
+ mounts: mountList,
5426
+ mountsHash: mountsDigest(mountList, image)
5427
+ };
5428
+ });
5429
+ }
5430
+ function listPassengerContainers(exec = realExec) {
5431
+ const out = exec("docker", ["ps", "-a", "--filter", "name=crew-passenger-", "--format", "{{.Names}} {{.State}}"]);
5432
+ return out.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
5433
+ const [name, state] = line.split(" ");
5434
+ return { name, running: state === "running" };
5435
+ });
5436
+ }
5437
+ function currentMountsHash(name, exec) {
5438
+ try {
5439
+ const out = exec("docker", ["inspect", "--format", '{{ index .Config.Labels "crew.mounts.hash" }}', name]);
5440
+ const hash2 = out.trim();
5441
+ return hash2 || void 0;
5442
+ } catch {
5443
+ return void 0;
5444
+ }
5445
+ }
5446
+ function stopContainer(name, exec = realExec) {
5447
+ try {
5448
+ exec("docker", ["rm", "-f", name]);
5449
+ } catch {
5450
+ }
5451
+ }
5452
+ function startContainer(plan, tablationApiBaseUrl, sync, exec = realExec) {
5453
+ const args = [
5454
+ "run",
5455
+ "-d",
5456
+ "--name",
5457
+ plan.containerName,
5458
+ "--restart",
5459
+ "unless-stopped",
5460
+ "-p",
5461
+ `${plan.port}:8765`,
5462
+ "-e",
5463
+ `PASSENGER_MCP_WORKSPACE_ID=${plan.workspaceId}`,
5464
+ "-e",
5465
+ `TABLATION_API_BASE_URL=${tablationApiBaseUrl}`,
5466
+ "--label",
5467
+ `crew.mounts.hash=${plan.mountsHash}`
5468
+ ];
5469
+ if (sync) {
5470
+ args.push(
5471
+ "--add-host",
5472
+ "host.docker.internal:host-gateway",
5473
+ "-e",
5474
+ `PASSENGER_SYNC_URL=http://${sync.host}:${sync.port}/sync`,
5475
+ "-e",
5476
+ `PASSENGER_SYNC_SECRET=${sync.secret}`,
5477
+ "-e",
5478
+ `PASSENGER_MCP_SECRET=${sync.secret}`
5479
+ );
5480
+ }
5481
+ for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
5482
+ args.push(plan.image);
5483
+ exec("docker", args);
5484
+ }
5485
+ function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
5486
+ const plans = planContainers(cfg);
5487
+ const plannedByName = new Map(plans.map((p) => [p.containerName, p]));
5488
+ const existing = listPassengerContainers(exec);
5489
+ const existingByName = new Map(existing.map((c) => [c.name, c]));
5490
+ const started = [];
5491
+ const recreated = [];
5492
+ const stopped = [];
5493
+ const ensured = /* @__PURE__ */ new Set();
5494
+ const start = (plan, sync) => {
5495
+ if (!ensured.has(plan.image)) {
5496
+ ensureImage(plan.image, exec);
5497
+ ensured.add(plan.image);
5498
+ }
5499
+ startContainer(plan, tablationApiBaseUrl, sync, exec);
5500
+ };
5501
+ for (const plan of plans) {
5502
+ const sync = syncEndpoints.get(plan.workspaceId);
5503
+ const current = existingByName.get(plan.containerName);
5504
+ if (!current) {
5505
+ start(plan, sync);
5506
+ started.push(plan.containerName);
5507
+ } else if (!current.running) {
5508
+ stopContainer(plan.containerName, exec);
5509
+ start(plan, sync);
5510
+ started.push(plan.containerName);
5511
+ } else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
5512
+ stopContainer(plan.containerName, exec);
5513
+ start(plan, sync);
5514
+ recreated.push(plan.containerName);
5515
+ }
5516
+ }
5517
+ for (const { name } of existing) {
5518
+ if (!plannedByName.has(name)) {
5519
+ stopContainer(name, exec);
5520
+ stopped.push(name);
5521
+ }
5522
+ }
5523
+ return { started, recreated, stopped };
5524
+ }
5525
+ function passengerCheckoutSyncSpecs(cfg, workspaceId) {
5526
+ const specs = [];
5527
+ for (const route2 of cfg.routes) {
5528
+ if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
5529
+ if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
5530
+ const routeWorkspaceId = route2.resolved.workspaceId;
5531
+ const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
5532
+ for (const target of passengerCheckoutTargets(route2)) {
5533
+ const ordinary = ordinaryByName.get(target.name);
5534
+ const override = route2.repoOverrides[target.name];
5535
+ const effective = resolveRepoConfig(
5536
+ ordinary ? loadRepoConfig(ordinary.dir) : null,
5537
+ { branch: { ...route2.branch, ...override?.branch } },
5538
+ ordinary?.dir ?? target.dir
5539
+ );
5540
+ specs.push({
5541
+ name: target.name,
5542
+ workspaceId: routeWorkspaceId,
5543
+ dir: target.dir,
5544
+ remote: target.remote,
5545
+ gitRemoteName: effective.branch.remote,
5546
+ base: effective.branch.base,
5547
+ originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
5548
+ cloneScheme: cfg.ship.cloneScheme,
5549
+ ...target.forgeHost ? { forgeHost: target.forgeHost } : {}
5550
+ });
5551
+ }
5552
+ }
5553
+ return specs;
5554
+ }
5555
+ function syncAllPassengerCheckouts(cfg) {
5556
+ return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
5557
+ name: spec.name,
5558
+ workspaceId: spec.workspaceId,
5559
+ outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
5560
+ }));
5561
+ }
5562
+
5563
+ // src/sandbox-image.ts
5564
+ var SANDBOX_BASE_IMAGE = "nvcr.io/nvidia/base/ubuntu:24.04";
5565
+ var SANDBOX_HARNESS_VERSION = "0.1.6";
5566
+ var SANDBOX_UID = 1e3;
5567
+ var CREW_PREFIX = "/opt/crew";
5568
+ var SANDBOX_RETRY_AFTER_MS = 60 * 60 * 1e3;
5569
+ var KEEP_TAGS = 2;
5570
+ var CREDENTIAL_HELPER = `#!/bin/sh
5571
+ # git credential helper: answers from the environment the runner passes in, so
5572
+ # no token is ever written into an image layer.
5573
+ [ "$1" = get ] || exit 0
5574
+ [ -n "$CREW_FORGE_TOKEN" ] || exit 0
5575
+ echo "username=\${CREW_FORGE_USER:-x-access-token}"
5576
+ echo "password=$CREW_FORGE_TOKEN"
5577
+ `;
5578
+ var MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
5579
+ `;
5580
+ var hash = (parts) => createHash5("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 12);
5581
+ var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
5582
+ var run = (cmd) => `RUN ["/bin/bash", "-c", ${JSON.stringify(`set -euo pipefail; ${cmd}`)}]`;
5583
+ var CREW_ENV = {
5584
+ CREW_SANDBOX: "1",
5585
+ CREW_CACHE_DIR: `${CREW_PREFIX}/cache`,
5586
+ XDG_CACHE_HOME: `${CREW_PREFIX}/cache`,
5587
+ npm_config_cache: `${CREW_PREFIX}/cache/npm`,
5588
+ npm_config_store_dir: `${CREW_PREFIX}/pnpm-store`,
5589
+ PNPM_HOME: `${CREW_PREFIX}/pnpm`
5590
+ };
5591
+ function baseLayer() {
5592
+ const dockerfile = [
5593
+ `FROM ${SANDBOX_BASE_IMAGE}`,
5594
+ `LABEL io.crew.sandbox.layer="base" io.crew.sandbox.harness="${SANDBOX_HARNESS_VERSION}"`,
5595
+ ""
5596
+ ].join("\n");
5597
+ return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_HARNESS_VERSION })}`, dockerfile, files: {} };
5598
+ }
5599
+ function crewLayer(base) {
5600
+ const files = { "credential-helper": CREDENTIAL_HELPER, "mcp.json": MCP_CONFIG };
5601
+ const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
5602
+ const dockerfile = [
5603
+ `FROM ${base.tag}`,
5604
+ "USER root",
5605
+ run("apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*"),
5606
+ `COPY credential-helper ${CREW_PREFIX}/bin/credential-helper`,
5607
+ `COPY mcp.json ${CREW_PREFIX}/mcp/mcp.json`,
5608
+ run(`chmod 0755 ${CREW_PREFIX}/bin/credential-helper && git config --system credential.helper ${CREW_PREFIX}/bin/credential-helper`),
5609
+ run(`mkdir -p ${CREW_PREFIX}/cache ${CREW_PREFIX}/pnpm-store ${CREW_PREFIX}/pnpm /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} ${CREW_PREFIX} /sandbox`),
5610
+ `ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
5611
+ `LABEL io.crew.sandbox.layer="crew"`,
5612
+ ""
5613
+ ].join("\n");
5614
+ return { name: "crew", tag: `crew-sandbox-crew:${hash({ base: base.tag, dockerfile, files })}`, dockerfile, files };
5615
+ }
5616
+ function repoLayer(repo, decl, crew) {
5617
+ const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
5618
+ const lines2 = [];
5619
+ if (decl.image) {
5620
+ lines2.push(
5621
+ `FROM ${crew.tag} AS crew`,
5622
+ `FROM ${decl.image}`,
5623
+ "USER root",
5624
+ `COPY --from=crew ${CREW_PREFIX} ${CREW_PREFIX}`,
5625
+ `ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:$PATH`
5626
+ );
5627
+ } else {
5628
+ lines2.push(`FROM ${crew.tag}`, "USER root");
5629
+ }
5630
+ if (decl.packages.length > 0) {
5631
+ lines2.push(run(`apt-get update && apt-get install -y --no-install-recommends ${decl.packages.join(" ")} && rm -rf /var/lib/apt/lists/*`));
5632
+ }
5633
+ lines2.push(
5634
+ run(`mkdir -p /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} /sandbox ${CREW_PREFIX}`),
5635
+ `USER ${SANDBOX_UID}`,
5636
+ "WORKDIR /sandbox"
5637
+ );
5638
+ for (const step of decl.setup) lines2.push(run(step));
5639
+ lines2.push(`LABEL io.crew.sandbox.layer="repo" io.crew.sandbox.repo=${JSON.stringify(repo)}`, "");
5640
+ const dockerfile = lines2.join("\n");
5641
+ return { name: "repo", tag: `crew-sandbox/${slug(repo)}:${hash({ crew: crew.tag, dockerfile })}`, dockerfile, files: {} };
5642
+ }
5643
+ function planSandbox(repo, decl) {
5644
+ const base = baseLayer();
5645
+ const crew = crewLayer(base);
5646
+ const layer = repoLayer(repo, decl, crew);
5647
+ return { repo, tag: layer.tag, layers: [base, crew, layer] };
5648
+ }
5649
+ var sandboxStatePath = (stateDir) => join15(stateDir, "sandbox-images.json");
5650
+ function readSandboxState(stateDir) {
5651
+ try {
5652
+ const v = JSON.parse(readFileSync14(sandboxStatePath(stateDir), "utf8"));
5653
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
5654
+ } catch {
5655
+ return {};
5656
+ }
5657
+ }
5658
+ function writeSandboxState(stateDir, state) {
5659
+ mkdirSync9(stateDir, { recursive: true });
5660
+ const file = sandboxStatePath(stateDir);
5661
+ writeFileSync10(`${file}.tmp`, `${JSON.stringify(state, null, 2)}
5662
+ `);
5663
+ renameSync3(`${file}.tmp`, file);
5664
+ }
5665
+ function sandboxImageStatus(state, plan) {
5666
+ const s = state[plan.repo];
5667
+ const current = s?.tags[0];
5668
+ if (s?.failure && s.failure.tag === plan.tag) {
5669
+ return { status: "failed", detail: `last build of ${plan.tag} failed at ${s.failure.at}; log ${s.failure.logPath}${current ? `; running ${current.tag}` : ""}` };
5670
+ }
5671
+ if (!current) return { status: "missing", detail: `no image built yet (wants ${plan.tag}); run \`crew sandbox build\`` };
5672
+ if (current.tag !== plan.tag) return { status: "stale", detail: `running ${current.tag}, declaration now builds ${plan.tag}` };
5673
+ return { status: "ok", detail: `${current.tag} built ${current.builtAt}` };
5674
+ }
5675
+ var spawnBuildRunner = (engine = "docker") => (args, logPath) => new Promise((resolve7) => {
5676
+ const child = spawn2(engine, args, { stdio: ["ignore", "pipe", "pipe"] });
5677
+ const chunks = [];
5678
+ child.stdout.on("data", (d) => chunks.push(d));
5679
+ child.stderr.on("data", (d) => chunks.push(d));
5680
+ const done = (code, extra = "") => {
5681
+ writeFileSync10(logPath, `$ ${engine} ${args.join(" ")}
5682
+ ${Buffer.concat(chunks).toString("utf8")}${extra}`, { flag: "a" });
5683
+ resolve7(code);
5684
+ };
5685
+ child.on("error", (e) => done(127, `${e.message}
5686
+ `));
5687
+ child.on("close", (code) => done(code ?? 1));
5688
+ });
5689
+ var engineHasImage = (engine = "docker", exec = realExec) => (tag) => {
5690
+ try {
5691
+ exec(engine, ["image", "inspect", tag], { timeoutMs: 2e4 });
5692
+ return true;
5693
+ } catch {
5694
+ return false;
5695
+ }
5696
+ };
5697
+ var engineRemoveImage = (engine = "docker", exec = realExec) => (tag) => {
5698
+ try {
5699
+ exec(engine, ["image", "rm", tag], { timeoutMs: 6e4 });
5700
+ } catch {
5701
+ }
5702
+ };
5703
+ var sandboxLogDir = (stateDir) => join15(stateDir, "sandbox-logs");
5704
+ async function buildSandboxImage(plan, deps) {
5705
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
5706
+ const state = readSandboxState(deps.stateDir);
5707
+ const entry = state[plan.repo] ?? { tags: [] };
5708
+ if (entry.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag)) {
5709
+ return { kind: "reused", tag: plan.tag };
5710
+ }
5711
+ const logDir = sandboxLogDir(deps.stateDir);
5712
+ mkdirSync9(logDir, { recursive: true });
5713
+ const logPath = join15(logDir, `${slug(plan.repo)}-${now().toISOString().replace(/[:.]/g, "-")}.log`);
5714
+ const builtLayers = [];
5715
+ for (const layer of plan.layers) {
5716
+ if (deps.hasImage(layer.tag)) continue;
5717
+ const ctx = join15(tmpdir2(), `crew-sandbox-${hash([layer.tag, process.pid, Date.now()])}`);
5718
+ mkdirSync9(ctx, { recursive: true });
5719
+ try {
5720
+ writeFileSync10(join15(ctx, "Dockerfile"), layer.dockerfile);
5721
+ for (const [name, content] of Object.entries(layer.files)) writeFileSync10(join15(ctx, name), content);
5722
+ const code = await deps.run(["build", "--tag", layer.tag, "--file", join15(ctx, "Dockerfile"), ctx], logPath);
5723
+ if (code !== 0) {
5724
+ state[plan.repo] = { ...entry, failure: { tag: plan.tag, at: now().toISOString(), logPath } };
5725
+ writeSandboxState(deps.stateDir, state);
5726
+ return { kind: "failed", tag: plan.tag, logPath, layer: layer.name };
5727
+ }
5728
+ builtLayers.push(layer.name);
5729
+ } finally {
5730
+ rmSync9(ctx, { recursive: true, force: true });
5731
+ }
5732
+ }
5733
+ const tags = [{ tag: plan.tag, builtAt: now().toISOString() }, ...entry.tags.filter((t) => t.tag !== plan.tag)];
5734
+ const dropped = tags.slice(KEEP_TAGS);
5735
+ state[plan.repo] = { tags: tags.slice(0, KEEP_TAGS) };
5736
+ writeSandboxState(deps.stateDir, state);
5737
+ for (const t of dropped) deps.removeImage(t.tag);
5738
+ return { kind: "built", tag: plan.tag, builtLayers };
5739
+ }
5740
+ var sandboxFailedKey = (repo) => `sandbox_build_failed:${repo}`;
5741
+ function sandboxFailedItem(repo, tag, layer, logPath, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
5742
+ return {
5743
+ kind: "sandbox_build_failed",
5744
+ key: sandboxFailedKey(repo),
5745
+ since: nowIso,
5746
+ message: `${repo}: sandbox image ${tag} failed to build at the ${layer} layer (log: ${logPath}); sandboxes keep running the previous image. Retries automatically after the declaration changes or in an hour; \`crew sandbox build\` forces it`
5747
+ };
5748
+ }
5749
+ async function sandboxBuildTick(repos, deps, force = false) {
5750
+ const nowMs = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
5751
+ for (const r of repos) {
5752
+ const plan = planSandbox(r.name, r.decl);
5753
+ const state = readSandboxState(deps.stateDir)[r.name];
5754
+ const upToDate = state?.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag);
5755
+ if (upToDate) {
5756
+ deps.clear(sandboxFailedKey(r.name));
5757
+ continue;
5758
+ }
5759
+ if (!force && state?.failure?.tag === plan.tag && nowMs - Date.parse(state.failure.at) < SANDBOX_RETRY_AFTER_MS) continue;
5760
+ deps.log(`sandbox image for ${r.name}: building ${plan.tag}`);
5761
+ const out = await buildSandboxImage(plan, deps);
5762
+ if (out.kind === "failed") {
5763
+ deps.raise(sandboxFailedItem(r.name, out.tag, out.layer, out.logPath));
5764
+ deps.log(`sandbox image for ${r.name}: build failed at the ${out.layer} layer, log ${out.logPath}`);
5765
+ } else {
5766
+ deps.clear(sandboxFailedKey(r.name));
5767
+ deps.log(`sandbox image for ${r.name}: ${out.kind === "built" ? `built ${out.tag}` : `${out.tag} already built`}`);
5768
+ }
5769
+ return r.name;
5770
+ }
5771
+ return null;
5772
+ }
5773
+
5774
+ // src/doctor-checks.ts
5775
+ import { execFileSync as execFileSync4 } from "child_process";
5318
5776
  var PROBE_BRANCH = "crew-probe-branch";
5319
5777
  var realGit = (cwd, args, env) => {
5320
- execFileSync3("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5778
+ execFileSync4("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5321
5779
  };
5322
5780
  function isSshRemote(url) {
5323
5781
  return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
@@ -5325,12 +5783,12 @@ function isSshRemote(url) {
5325
5783
  function pushAuthHint(remoteUrl) {
5326
5784
  return isSshRemote(remoteUrl) ? "make sure an SSH key is loaded for this host (ssh-add), and that the scheduler user can reach it" : "run `gh auth login`, then `gh auth setup-git`";
5327
5785
  }
5328
- function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
5786
+ function probePushAuth(o, run3 = realGit, baseEnv = process.env) {
5329
5787
  if (o.offline) return { state: "skipped", detail: "skipped (--offline)" };
5330
5788
  if (!o.remoteUrl) return { state: "skipped", detail: `no remote "${o.remote}" configured` };
5331
5789
  const env = { ...baseEnv, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_SSH_COMMAND: `${baseEnv.GIT_SSH_COMMAND ?? "ssh"} -o BatchMode=yes` };
5332
5790
  try {
5333
- run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
5791
+ run3(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
5334
5792
  return { state: "ok" };
5335
5793
  } catch (e) {
5336
5794
  const err3 = e;
@@ -5346,7 +5804,7 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
5346
5804
  };
5347
5805
  }
5348
5806
  try {
5349
- run2(o.dir, ["ls-remote", "--exit-code", o.remote, "HEAD"], env);
5807
+ run3(o.dir, ["ls-remote", "--exit-code", o.remote, "HEAD"], env);
5350
5808
  return {
5351
5809
  state: "failed",
5352
5810
  detail: `the token for forge ${f.host} is read-only (fetch works, push refused): ${detail}`,
@@ -5438,31 +5896,31 @@ function roleCapWarnings(ship) {
5438
5896
  }
5439
5897
 
5440
5898
  // src/selftest.ts
5441
- import { spawn as spawn4 } from "child_process";
5442
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync20, rmSync as rmSync13, writeFileSync as writeFileSync15 } from "fs";
5443
- import { tmpdir as tmpdir3 } from "os";
5444
- import { join as join24 } from "path";
5899
+ import { spawn as spawn5 } from "child_process";
5900
+ import { existsSync as existsSync22, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, readFileSync as readFileSync21, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
5901
+ import { tmpdir as tmpdir4 } from "os";
5902
+ import { join as join25 } from "path";
5445
5903
 
5446
5904
  // src/agent.ts
5447
5905
  import { spawnSync as spawnSync5 } from "child_process";
5448
- import { readFileSync as readFileSync19, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync12, writeFileSync as writeFileSync14, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5906
+ import { readFileSync as readFileSync20, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5449
5907
  import { StringDecoder } from "string_decoder";
5450
5908
  import { finished } from "stream/promises";
5451
- import { join as join23, dirname as dirname8, basename as basename8 } from "path";
5452
- import { createHash as createHash4 } from "crypto";
5909
+ import { join as join24, dirname as dirname8, basename as basename8 } from "path";
5910
+ import { createHash as createHash6 } from "crypto";
5453
5911
 
5454
5912
  // src/state.ts
5455
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9, rmSync as rmSync9, statSync as statSync6, readdirSync as readdirSync5 } from "fs";
5456
- import { join as join15 } from "path";
5913
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync10, rmSync as rmSync10, statSync as statSync6, readdirSync as readdirSync5 } from "fs";
5914
+ import { join as join16 } from "path";
5457
5915
  var EPOCH = "1970-01-01T00:00:00.000Z";
5458
5916
  var State = class _State {
5459
5917
  dir;
5460
5918
  constructor(dir) {
5461
5919
  this.dir = dir;
5462
- mkdirSync9(dir, { recursive: true });
5920
+ mkdirSync10(dir, { recursive: true });
5463
5921
  }
5464
5922
  path(...parts) {
5465
- return join15(this.dir, ...parts);
5923
+ return join16(this.dir, ...parts);
5466
5924
  }
5467
5925
  /**
5468
5926
  * The timestamp every "new since last poll" check is measured against.
@@ -5473,14 +5931,14 @@ var State = class _State {
5473
5931
  */
5474
5932
  watermark() {
5475
5933
  try {
5476
- const v = readFileSync14(this.path(".poll-watermark"), "utf8").trim();
5934
+ const v = readFileSync15(this.path(".poll-watermark"), "utf8").trim();
5477
5935
  return v || EPOCH;
5478
5936
  } catch {
5479
5937
  return EPOCH;
5480
5938
  }
5481
5939
  }
5482
5940
  setWatermark(at) {
5483
- writeFileSync10(this.path(".poll-watermark"), `${at}
5941
+ writeFileSync11(this.path(".poll-watermark"), `${at}
5484
5942
  `);
5485
5943
  }
5486
5944
  /**
@@ -5507,10 +5965,10 @@ var State = class _State {
5507
5965
  return new Set(roles.filter((r) => this.isRolePaused(r)));
5508
5966
  }
5509
5967
  pause(role) {
5510
- writeFileSync10(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5968
+ writeFileSync11(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5511
5969
  }
5512
5970
  resume(role) {
5513
- rmSync9(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
5971
+ rmSync10(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
5514
5972
  }
5515
5973
  /**
5516
5974
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -5521,8 +5979,8 @@ var State = class _State {
5521
5979
  return existsSync14(this.path(".release-paused"));
5522
5980
  }
5523
5981
  setReleasePaused(paused) {
5524
- if (paused) writeFileSync10(this.path(".release-paused"), "");
5525
- else rmSync9(this.path(".release-paused"), { force: true });
5982
+ if (paused) writeFileSync11(this.path(".release-paused"), "");
5983
+ else rmSync10(this.path(".release-paused"), { force: true });
5526
5984
  }
5527
5985
  /**
5528
5986
  * Why a release cycle must not run right now, or null when it may. Every
@@ -5559,7 +6017,7 @@ var State = class _State {
5559
6017
  const name = _State.safe(rawName);
5560
6018
  const file = this.path(`.${name}.lock`);
5561
6019
  try {
5562
- const existing = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
6020
+ const existing = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
5563
6021
  if (Number.isFinite(existing) && existing > 0) {
5564
6022
  try {
5565
6023
  process.kill(existing, 0);
@@ -5569,13 +6027,13 @@ var State = class _State {
5569
6027
  }
5570
6028
  } catch {
5571
6029
  }
5572
- writeFileSync10(file, `${process.pid}
6030
+ writeFileSync11(file, `${process.pid}
5573
6031
  `);
5574
6032
  return {
5575
6033
  ok: true,
5576
6034
  release: () => {
5577
6035
  try {
5578
- rmSync9(file, { force: true });
6036
+ rmSync10(file, { force: true });
5579
6037
  } catch {
5580
6038
  }
5581
6039
  }
@@ -5597,7 +6055,7 @@ var State = class _State {
5597
6055
  if (!/^\.release-.+\.lock$/.test(n)) continue;
5598
6056
  let pid = 0;
5599
6057
  try {
5600
- pid = Number.parseInt(readFileSync14(this.path(n), "utf8").trim(), 10);
6058
+ pid = Number.parseInt(readFileSync15(this.path(n), "utf8").trim(), 10);
5601
6059
  } catch {
5602
6060
  continue;
5603
6061
  }
@@ -5683,7 +6141,7 @@ var State = class _State {
5683
6141
  ticketInFlight(recordId) {
5684
6142
  const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
5685
6143
  try {
5686
- const pid = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
6144
+ const pid = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
5687
6145
  if (!Number.isFinite(pid) || pid <= 0) return false;
5688
6146
  process.kill(pid, 0);
5689
6147
  return true;
@@ -5717,7 +6175,7 @@ var State = class _State {
5717
6175
  const heldReported = `.release-held-${suffix}`;
5718
6176
  const read = (f) => {
5719
6177
  try {
5720
- return readFileSync14(this.path(f), "utf8").trim() || null;
6178
+ return readFileSync15(this.path(f), "utf8").trim() || null;
5721
6179
  } catch {
5722
6180
  return null;
5723
6181
  }
@@ -5726,12 +6184,12 @@ var State = class _State {
5726
6184
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
5727
6185
  noteBlocked: () => {
5728
6186
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
5729
- writeFileSync10(this.path(blocked), `${n}
6187
+ writeFileSync11(this.path(blocked), `${n}
5730
6188
  `);
5731
6189
  return n;
5732
6190
  },
5733
6191
  clearBlocked: () => {
5734
- rmSync9(this.path(blocked), { force: true });
6192
+ rmSync10(this.path(blocked), { force: true });
5735
6193
  },
5736
6194
  /**
5737
6195
  * The commit a deploy last failed on.
@@ -5743,11 +6201,11 @@ var State = class _State {
5743
6201
  */
5744
6202
  deployFailedSha: () => read(failed),
5745
6203
  noteDeployFailed: (sha) => {
5746
- writeFileSync10(this.path(failed), `${sha}
6204
+ writeFileSync11(this.path(failed), `${sha}
5747
6205
  `);
5748
6206
  },
5749
6207
  clearDeployFailed: () => {
5750
- rmSync9(this.path(failed), { force: true });
6208
+ rmSync10(this.path(failed), { force: true });
5751
6209
  },
5752
6210
  /**
5753
6211
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -5757,31 +6215,31 @@ var State = class _State {
5757
6215
  noteTestGateFailed: (sha) => {
5758
6216
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
5759
6217
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
5760
- writeFileSync10(this.path(gateFailed), `${sha} ${next}
6218
+ writeFileSync11(this.path(gateFailed), `${sha} ${next}
5761
6219
  `);
5762
6220
  return next;
5763
6221
  },
5764
6222
  clearTestGateFailed: () => {
5765
- rmSync9(this.path(gateFailed), { force: true });
6223
+ rmSync10(this.path(gateFailed), { force: true });
5766
6224
  },
5767
6225
  /** Whether the board was already told about this head's red gate. */
5768
6226
  heldReported: () => {
5769
6227
  try {
5770
- return readFileSync14(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
6228
+ return readFileSync15(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5771
6229
  } catch {
5772
6230
  return null;
5773
6231
  }
5774
6232
  },
5775
6233
  noteHeldReported: (signature) => {
5776
- writeFileSync10(this.path(heldReported), `${signature}
6234
+ writeFileSync11(this.path(heldReported), `${signature}
5777
6235
  `);
5778
6236
  },
5779
6237
  clearHeldReported: () => {
5780
- rmSync9(this.path(heldReported), { force: true });
6238
+ rmSync10(this.path(heldReported), { force: true });
5781
6239
  },
5782
6240
  testGateReported: (sha) => read(gateReported) === sha,
5783
6241
  noteTestGateReported: (sha) => {
5784
- writeFileSync10(this.path(gateReported), `${sha}
6242
+ writeFileSync11(this.path(gateReported), `${sha}
5785
6243
  `);
5786
6244
  }
5787
6245
  };
@@ -5804,13 +6262,13 @@ var State = class _State {
5804
6262
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
5805
6263
  const read = (f) => {
5806
6264
  try {
5807
- return readFileSync14(this.path(f), "utf8").trim() || null;
6265
+ return readFileSync15(this.path(f), "utf8").trim() || null;
5808
6266
  } catch {
5809
6267
  return null;
5810
6268
  }
5811
6269
  };
5812
6270
  const clear2 = () => {
5813
- for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync9(this.path(f), { force: true });
6271
+ for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync10(this.path(f), { force: true });
5814
6272
  };
5815
6273
  return {
5816
6274
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -5837,18 +6295,18 @@ var State = class _State {
5837
6295
  clear2();
5838
6296
  return;
5839
6297
  }
5840
- writeFileSync10(this.path(streakFile), `${this.readInt(streakFile) + 1}
6298
+ writeFileSync11(this.path(streakFile), `${this.readInt(streakFile) + 1}
5841
6299
  `);
5842
6300
  const prior = read(ticketFile);
5843
6301
  if (topTicket && topTicket === prior) {
5844
- writeFileSync10(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
6302
+ writeFileSync11(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5845
6303
  `);
5846
6304
  } else if (topTicket) {
5847
- writeFileSync10(this.path(ticketFile), `${topTicket}
6305
+ writeFileSync11(this.path(ticketFile), `${topTicket}
5848
6306
  `);
5849
- writeFileSync10(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
6307
+ writeFileSync11(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5850
6308
  `);
5851
- writeFileSync10(this.path(ticketStreakFile), "1\n");
6309
+ writeFileSync11(this.path(ticketStreakFile), "1\n");
5852
6310
  }
5853
6311
  }
5854
6312
  };
@@ -5869,14 +6327,14 @@ var State = class _State {
5869
6327
  return {
5870
6328
  previous: () => {
5871
6329
  try {
5872
- const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
6330
+ const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
5873
6331
  return parsed && typeof parsed === "object" ? parsed : {};
5874
6332
  } catch {
5875
6333
  return {};
5876
6334
  }
5877
6335
  },
5878
6336
  persist: (next) => {
5879
- writeFileSync10(this.path(file), JSON.stringify(next));
6337
+ writeFileSync11(this.path(file), JSON.stringify(next));
5880
6338
  }
5881
6339
  };
5882
6340
  }
@@ -5892,14 +6350,14 @@ var State = class _State {
5892
6350
  return {
5893
6351
  previous: () => {
5894
6352
  try {
5895
- const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
6353
+ const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
5896
6354
  return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
5897
6355
  } catch {
5898
6356
  return [];
5899
6357
  }
5900
6358
  },
5901
6359
  persist: (next) => {
5902
- writeFileSync10(this.path(file), JSON.stringify(next));
6360
+ writeFileSync11(this.path(file), JSON.stringify(next));
5903
6361
  }
5904
6362
  };
5905
6363
  }
@@ -5913,14 +6371,14 @@ var State = class _State {
5913
6371
  return {
5914
6372
  previous: () => {
5915
6373
  try {
5916
- const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
6374
+ const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
5917
6375
  return Array.isArray(parsed) ? parsed : [];
5918
6376
  } catch {
5919
6377
  return [];
5920
6378
  }
5921
6379
  },
5922
6380
  persist: (next) => {
5923
- writeFileSync10(this.path(file), JSON.stringify(next));
6381
+ writeFileSync11(this.path(file), JSON.stringify(next));
5924
6382
  }
5925
6383
  };
5926
6384
  }
@@ -5933,7 +6391,7 @@ var State = class _State {
5933
6391
  const file = ".release-last-run";
5934
6392
  return {
5935
6393
  touch: () => {
5936
- writeFileSync10(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
6394
+ writeFileSync11(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5937
6395
  },
5938
6396
  at: () => {
5939
6397
  try {
@@ -5946,7 +6404,7 @@ var State = class _State {
5946
6404
  }
5947
6405
  readInt(file) {
5948
6406
  try {
5949
- return Number.parseInt(readFileSync14(this.path(file), "utf8").trim(), 10) || 0;
6407
+ return Number.parseInt(readFileSync15(this.path(file), "utf8").trim(), 10) || 0;
5950
6408
  } catch {
5951
6409
  return 0;
5952
6410
  }
@@ -6429,8 +6887,8 @@ var PERISCOPE_COLUMNS = [
6429
6887
  ];
6430
6888
 
6431
6889
  // src/agent-log.ts
6432
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
6433
- import { join as join16 } from "path";
6890
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "fs";
6891
+ import { join as join17 } from "path";
6434
6892
 
6435
6893
  // src/pending-backoff.ts
6436
6894
  var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
@@ -6487,14 +6945,14 @@ async function findTicketRecord(target, t, ref) {
6487
6945
  );
6488
6946
  return rows.length === 1 ? rows[0].id : void 0;
6489
6947
  }
6490
- function counterIncrements(run2, cols, have) {
6948
+ function counterIncrements(run3, cols, have) {
6491
6949
  const figures = [
6492
6950
  [cols.agentRuns, 1],
6493
- [cols.tokensIn, run2.tokensIn],
6494
- [cols.tokensOut, run2.tokensOut],
6495
- [cols.cacheReadTokens, run2.cacheReadTokens],
6496
- [cols.cacheWriteTokens, run2.cacheWriteTokens],
6497
- [cols.costUsd, run2.costUsd]
6951
+ [cols.tokensIn, run3.tokensIn],
6952
+ [cols.tokensOut, run3.tokensOut],
6953
+ [cols.cacheReadTokens, run3.cacheReadTokens],
6954
+ [cols.cacheWriteTokens, run3.cacheWriteTokens],
6955
+ [cols.costUsd, run3.costUsd]
6498
6956
  ];
6499
6957
  const out = {};
6500
6958
  for (const [column, value2] of figures) {
@@ -6517,41 +6975,41 @@ async function post(target, path, body) {
6517
6975
  if (!res.ok) throw new AgentLogHttpError(`${path}: ${res.status} ${res.statusText}`, res.status);
6518
6976
  return await res.json();
6519
6977
  }
6520
- async function reportAgentRun(target, run2) {
6978
+ async function reportAgentRun(target, run3) {
6521
6979
  let ticketRecordId;
6522
- if (target.tickets && run2.ticketReference) {
6980
+ if (target.tickets && run3.ticketReference) {
6523
6981
  try {
6524
- ticketRecordId = await findTicketRecord(target, target.tickets, run2.ticketReference);
6982
+ ticketRecordId = await findTicketRecord(target, target.tickets, run3.ticketReference);
6525
6983
  } catch {
6526
6984
  }
6527
6985
  }
6528
6986
  const body = {
6529
6987
  agentId: target.agentId,
6530
- ticketReference: run2.ticketReference,
6531
- outcome: run2.outcome,
6532
- startedAt: run2.startedAt,
6533
- finishedAt: run2.finishedAt,
6988
+ ticketReference: run3.ticketReference,
6989
+ outcome: run3.outcome,
6990
+ startedAt: run3.startedAt,
6991
+ finishedAt: run3.finishedAt,
6534
6992
  source: "client",
6535
6993
  client: "crew",
6536
- model: run2.model,
6537
- tokensIn: run2.tokensIn,
6538
- tokensOut: run2.tokensOut,
6539
- cacheReadTokens: run2.cacheReadTokens,
6540
- cacheWriteTokens: run2.cacheWriteTokens,
6541
- costUsd: run2.costUsd,
6542
- numTurns: run2.numTurns,
6543
- prefixTokens: run2.prefixTokens,
6544
- promptVersion: run2.promptVersion,
6545
- promptSha: run2.promptSha
6994
+ model: run3.model,
6995
+ tokensIn: run3.tokensIn,
6996
+ tokensOut: run3.tokensOut,
6997
+ cacheReadTokens: run3.cacheReadTokens,
6998
+ cacheWriteTokens: run3.cacheWriteTokens,
6999
+ costUsd: run3.costUsd,
7000
+ numTurns: run3.numTurns,
7001
+ prefixTokens: run3.prefixTokens,
7002
+ promptVersion: run3.promptVersion,
7003
+ promptSha: run3.promptSha
6546
7004
  };
6547
7005
  const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
6548
7006
  let entry;
6549
7007
  try {
6550
- entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run2.harness, provider: run2.provider });
7008
+ entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run3.harness, provider: run3.provider });
6551
7009
  } catch (e) {
6552
7010
  if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
6553
7011
  if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
6554
- if (e.status !== 400 || !run2.harness && !run2.provider && !ticketRecordId) {
7012
+ if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId) {
6555
7013
  e.row = true;
6556
7014
  throw e;
6557
7015
  }
@@ -6562,7 +7020,7 @@ async function reportAgentRun(target, run2) {
6562
7020
  throw e2;
6563
7021
  }
6564
7022
  }
6565
- for (const cycle of run2.cycles) {
7023
+ for (const cycle of run3.cycles) {
6566
7024
  await post(target, `/agents/log/${entry.id}/cycles`, {
6567
7025
  cycleIndex: cycle.cycleIndex,
6568
7026
  occurredAt: cycle.occurredAt,
@@ -6570,7 +7028,7 @@ async function reportAgentRun(target, run2) {
6570
7028
  ...cycle.role ? { role: cycle.role } : {}
6571
7029
  });
6572
7030
  }
6573
- const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run2) : void 0;
7031
+ const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
6574
7032
  return {
6575
7033
  agentLogId: entry.id,
6576
7034
  ...counters?.warning ? { counterWarning: counters.warning } : {},
@@ -6584,38 +7042,38 @@ async function writeCounters(target, w) {
6584
7042
  if (Object.keys(body).length === 0) return;
6585
7043
  await request(target, "PATCH", `/data-models/${w.issuesModelId}/records/${w.recordId}`, body);
6586
7044
  }
6587
- async function addRunToTicket(target, t, recordId, run2) {
7045
+ async function addRunToTicket(target, t, recordId, run3) {
6588
7046
  const write = {
6589
7047
  issuesModelId: t.issuesModelId,
6590
7048
  recordId,
6591
- ticketReference: run2.ticketReference,
7049
+ ticketReference: run3.ticketReference,
6592
7050
  counters: t.counters,
6593
- body: counterIncrements(run2, t.counters, new Set(Object.values(t.counters)))
7051
+ body: counterIncrements(run3, t.counters, new Set(Object.values(t.counters)))
6594
7052
  };
6595
7053
  try {
6596
7054
  await writeCounters(target, write);
6597
7055
  return {};
6598
7056
  } catch (e) {
6599
7057
  return {
6600
- warning: `could not add run spend to ${run2.ticketReference}: ${e.message}`,
7058
+ warning: `could not add run spend to ${run3.ticketReference}: ${e.message}`,
6601
7059
  ...isRetryable(e) ? { retry: write } : {}
6602
7060
  };
6603
7061
  }
6604
7062
  }
6605
- var queueDir = (stateDir) => join16(stateDir, "pending-agent-logs");
7063
+ var queueDir = (stateDir) => join17(stateDir, "pending-agent-logs");
6606
7064
  function queueAgentRun(stateDir, name, q) {
6607
- mkdirSync10(queueDir(stateDir), { recursive: true });
6608
- writeFileSync11(join16(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
7065
+ mkdirSync11(queueDir(stateDir), { recursive: true });
7066
+ writeFileSync12(join17(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
6609
7067
  }
6610
7068
  async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6611
7069
  const dir = queueDir(stateDir);
6612
7070
  if (!existsSync15(dir)) return 0;
6613
7071
  let sent = 0;
6614
7072
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
6615
- const path = join16(dir, f);
7073
+ const path = join17(dir, f);
6616
7074
  let q;
6617
7075
  try {
6618
- q = JSON.parse(readFileSync15(path, "utf8"));
7076
+ q = JSON.parse(readFileSync16(path, "utf8"));
6619
7077
  } catch {
6620
7078
  continue;
6621
7079
  }
@@ -6634,17 +7092,17 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
6634
7092
  attempts: 0,
6635
7093
  nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
6636
7094
  });
6637
- rmSync10(path, { force: true });
7095
+ rmSync11(path, { force: true });
6638
7096
  sent += 1;
6639
7097
  log.emit(`sent a queued Agent Log report (${f})`);
6640
7098
  } catch (e) {
6641
7099
  if (!(e instanceof AgentLogUnreachableError)) {
6642
- rmSync10(path, { force: true });
7100
+ rmSync11(path, { force: true });
6643
7101
  log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
6644
7102
  continue;
6645
7103
  }
6646
7104
  const attempts = q.attempts + 1;
6647
- writeFileSync11(path, JSON.stringify({
7105
+ writeFileSync12(path, JSON.stringify({
6648
7106
  ...q,
6649
7107
  attempts,
6650
7108
  warned: true,
@@ -6655,20 +7113,20 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
6655
7113
  }
6656
7114
  return sent;
6657
7115
  }
6658
- var countersDir = (stateDir) => join16(stateDir, "pending-ticket-counters");
7116
+ var countersDir = (stateDir) => join17(stateDir, "pending-ticket-counters");
6659
7117
  function queueTicketCounters(stateDir, name, q) {
6660
- mkdirSync10(countersDir(stateDir), { recursive: true });
6661
- writeFileSync11(join16(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
7118
+ mkdirSync11(countersDir(stateDir), { recursive: true });
7119
+ writeFileSync12(join17(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
6662
7120
  }
6663
7121
  async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6664
7122
  const dir = countersDir(stateDir);
6665
7123
  if (!existsSync15(dir)) return 0;
6666
7124
  let sent = 0;
6667
7125
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
6668
- const path = join16(dir, f);
7126
+ const path = join17(dir, f);
6669
7127
  let q;
6670
7128
  try {
6671
- q = JSON.parse(readFileSync15(path, "utf8"));
7129
+ q = JSON.parse(readFileSync16(path, "utf8"));
6672
7130
  } catch {
6673
7131
  continue;
6674
7132
  }
@@ -6677,17 +7135,17 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
6677
7135
  const apiKey = await keyFor(q.role);
6678
7136
  if (!apiKey) continue;
6679
7137
  await writeCounters({ ...q.target, apiKey }, q.write);
6680
- rmSync10(path, { force: true });
7138
+ rmSync11(path, { force: true });
6681
7139
  sent += 1;
6682
7140
  log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
6683
7141
  } catch (e) {
6684
7142
  if (!isRetryable(e)) {
6685
- rmSync10(path, { force: true });
7143
+ rmSync11(path, { force: true });
6686
7144
  log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
6687
7145
  continue;
6688
7146
  }
6689
7147
  const attempts = q.attempts + 1;
6690
- writeFileSync11(path, JSON.stringify({
7148
+ writeFileSync12(path, JSON.stringify({
6691
7149
  ...q,
6692
7150
  attempts,
6693
7151
  warned: true,
@@ -6890,8 +7348,8 @@ var HandoffTranscriptTracker = class {
6890
7348
  };
6891
7349
 
6892
7350
  // src/pending-handoff.ts
6893
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync16, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "fs";
6894
- import { join as join19 } from "path";
7351
+ import { existsSync as existsSync18, mkdirSync as mkdirSync12, readdirSync as readdirSync7, readFileSync as readFileSync17, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "fs";
7352
+ import { join as join20 } from "path";
6895
7353
 
6896
7354
  // src/ticket-branch.ts
6897
7355
  import { basename as basename6 } from "path";
@@ -8058,15 +8516,15 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
8058
8516
 
8059
8517
  // src/run-worktree.ts
8060
8518
  import { existsSync as existsSync17 } from "fs";
8061
- import { dirname as dirname7, join as join18 } from "path";
8519
+ import { dirname as dirname7, join as join19 } from "path";
8062
8520
 
8063
8521
  // src/ports.ts
8064
- import { execFileSync as execFileSync4 } from "child_process";
8522
+ import { execFileSync as execFileSync5 } from "child_process";
8065
8523
  import { existsSync as existsSync16 } from "fs";
8066
- import { join as join17 } from "path";
8067
- var run = (cmd, args) => {
8524
+ import { join as join18 } from "path";
8525
+ var run2 = (cmd, args) => {
8068
8526
  try {
8069
- return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8527
+ return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8070
8528
  } catch {
8071
8529
  return "";
8072
8530
  }
@@ -8080,7 +8538,7 @@ function listeners() {
8080
8538
  seen.add(key);
8081
8539
  out.push({ pid, port });
8082
8540
  };
8083
- const lsof = run("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
8541
+ const lsof = run2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
8084
8542
  if (lsof) {
8085
8543
  for (const line of lsof.split("\n").slice(1)) {
8086
8544
  const cols = line.split(/\s+/);
@@ -8090,7 +8548,7 @@ function listeners() {
8090
8548
  }
8091
8549
  return out;
8092
8550
  }
8093
- const ss = run("ss", ["-ltnpH"]);
8551
+ const ss = run2("ss", ["-ltnpH"]);
8094
8552
  for (const line of ss.split("\n")) {
8095
8553
  const local = line.trim().split(/\s+/)[3];
8096
8554
  const pid = /pid=(\d+)/.exec(line)?.[1];
@@ -8108,7 +8566,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
8108
8566
  }
8109
8567
  function worktreeExists(parent, prefix, n) {
8110
8568
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
8111
- if (existsSync16(join17(parent, name, ".git"))) return true;
8569
+ if (existsSync16(join18(parent, name, ".git"))) return true;
8112
8570
  }
8113
8571
  return false;
8114
8572
  }
@@ -8142,7 +8600,7 @@ async function killGently(pids, graceMs = 2e3) {
8142
8600
  function pidsInWorktree(path, n, s = DEFAULT_PORTS) {
8143
8601
  const pids = /* @__PURE__ */ new Set();
8144
8602
  for (const port of [s.backendBase + n, s.frontendBase + n]) for (const p of pidsOnPort(port)) pids.add(p);
8145
- const matched = run("pgrep", ["-f", `${path}/`]);
8603
+ const matched = run2("pgrep", ["-f", `${path}/`]);
8146
8604
  for (const line of matched.split("\n")) {
8147
8605
  const pid = Number(line.trim());
8148
8606
  if (Number.isFinite(pid) && pid > 0 && pid !== process.pid) pids.add(pid);
@@ -8207,7 +8665,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
8207
8665
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
8208
8666
  return { action: "recut", path: existing.path, branch, base: baseRef };
8209
8667
  }
8210
- const path = join18(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8668
+ const path = join19(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8211
8669
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
8212
8670
  return { action: "cut-remote", path, branch: located.branch };
8213
8671
  }
@@ -8664,12 +9122,12 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
8664
9122
 
8665
9123
  // src/pending-handoff.ts
8666
9124
  var PENDING_MAX_AGE_MS = 48 * 36e5;
8667
- var pendingDirFor = (stateDir) => join19(stateDir, "pending-handoffs");
8668
- var stateDirOfStream = (streamPath) => join19(streamPath, "..", "..");
9125
+ var pendingDirFor = (stateDir) => join20(stateDir, "pending-handoffs");
9126
+ var stateDirOfStream = (streamPath) => join20(streamPath, "..", "..");
8669
9127
  function writePendingHandoff(stateDir, rec) {
8670
9128
  const dir = pendingDirFor(stateDir);
8671
- mkdirSync11(dir, { recursive: true });
8672
- writeFileSync12(join19(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
9129
+ mkdirSync12(dir, { recursive: true });
9130
+ writeFileSync13(join20(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
8673
9131
  }
8674
9132
  function listPendingHandoffs(stateDir, route2) {
8675
9133
  const dir = pendingDirFor(stateDir);
@@ -8677,7 +9135,7 @@ function listPendingHandoffs(stateDir, route2) {
8677
9135
  const out = [];
8678
9136
  for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
8679
9137
  try {
8680
- const rec = JSON.parse(readFileSync16(join19(dir, f), "utf8"));
9138
+ const rec = JSON.parse(readFileSync17(join20(dir, f), "utf8"));
8681
9139
  if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
8682
9140
  } catch {
8683
9141
  }
@@ -8685,7 +9143,7 @@ function listPendingHandoffs(stateDir, route2) {
8685
9143
  return out;
8686
9144
  }
8687
9145
  function removePendingHandoff(stateDir, ticketRecordId) {
8688
- rmSync11(join19(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
9146
+ rmSync12(join20(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
8689
9147
  }
8690
9148
  function pendingHandoffFor(stateDir, ticketRecordId) {
8691
9149
  return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
@@ -8795,8 +9253,8 @@ function expandSkillMarkers(text2, lookup2, opts = {}) {
8795
9253
  }
8796
9254
 
8797
9255
  // src/skills.ts
8798
- import { readFileSync as readFileSync18, readdirSync as readdirSync8, existsSync as existsSync20 } from "fs";
8799
- import { join as join21 } from "path";
9256
+ import { readFileSync as readFileSync19, readdirSync as readdirSync8, existsSync as existsSync20 } from "fs";
9257
+ import { join as join22 } from "path";
8800
9258
  import { parse as parseYaml } from "yaml";
8801
9259
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
8802
9260
 
@@ -8812,8 +9270,8 @@ ${additions}`;
8812
9270
  }
8813
9271
 
8814
9272
  // src/agents.ts
8815
- import { readFileSync as readFileSync17, existsSync as existsSync19 } from "fs";
8816
- import { join as join20 } from "path";
9273
+ import { readFileSync as readFileSync18, existsSync as existsSync19 } from "fs";
9274
+ import { join as join21 } from "path";
8817
9275
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
8818
9276
  var PERSONA_NAME = {
8819
9277
  dev: "Developer",
@@ -8827,12 +9285,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8827
9285
  var AgentsSyncError = class extends Error {
8828
9286
  };
8829
9287
  function personaDefaultPrompt(promptsDir, role) {
8830
- const briefPath = join20(promptsDir, "personas", `lane-${role}.md`);
9288
+ const briefPath = join21(promptsDir, "personas", `lane-${role}.md`);
8831
9289
  if (!existsSync19(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8832
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync17(briefPath, "utf8");
8833
- const commonPath = join20(promptsDir, "personas", "common.md");
8834
- if (!existsSync19(commonPath)) return readFileSync17(briefPath, "utf8");
8835
- return `${readFileSync17(commonPath, "utf8")}${readFileSync17(briefPath, "utf8")}`;
9290
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync18(briefPath, "utf8");
9291
+ const commonPath = join21(promptsDir, "personas", "common.md");
9292
+ if (!existsSync19(commonPath)) return readFileSync18(briefPath, "utf8");
9293
+ return `${readFileSync18(commonPath, "utf8")}${readFileSync18(briefPath, "utf8")}`;
8836
9294
  }
8837
9295
  async function resolveAgentId(route2, role, opts) {
8838
9296
  const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
@@ -9279,7 +9737,7 @@ function describeCrewLink(o) {
9279
9737
  var SkillSyncError = class extends Error {
9280
9738
  };
9281
9739
  function parseSkillFile(path) {
9282
- const text2 = readFileSync18(path, "utf8");
9740
+ const text2 = readFileSync19(path, "utf8");
9283
9741
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text2);
9284
9742
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
9285
9743
  const frontmatter = match[1];
@@ -9292,9 +9750,9 @@ function parseSkillFile(path) {
9292
9750
  return { name: meta.name, description: meta.description, prompt: body, file: path };
9293
9751
  }
9294
9752
  function listLocalSkills(promptsDir) {
9295
- const dir = join21(promptsDir, "skills");
9753
+ const dir = join22(promptsDir, "skills");
9296
9754
  if (!existsSync20(dir)) return [];
9297
- return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join21(dir, f)));
9755
+ return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join22(dir, f)));
9298
9756
  }
9299
9757
  async function syncSkills(route2, opts) {
9300
9758
  if (!route2.resolved) {
@@ -9478,10 +9936,10 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
9478
9936
  }
9479
9937
 
9480
9938
  // src/runner.ts
9481
- import { spawn as spawn2, execFile, execFileSync as execFileSync5 } from "child_process";
9482
- import { mkdtempSync, writeFileSync as writeFileSync13, rmSync as rmSync12 } from "fs";
9483
- import { tmpdir as tmpdir2 } from "os";
9484
- import { basename as basename7, join as join22 } from "path";
9939
+ import { spawn as spawn3, execFile, execFileSync as execFileSync6 } from "child_process";
9940
+ import { mkdtempSync, writeFileSync as writeFileSync14, rmSync as rmSync13 } from "fs";
9941
+ import { tmpdir as tmpdir3 } from "os";
9942
+ import { basename as basename7, join as join23 } from "path";
9485
9943
  import { promisify } from "util";
9486
9944
 
9487
9945
  // src/run-limit.ts
@@ -9545,7 +10003,7 @@ var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
9545
10003
  var HostRunner = class {
9546
10004
  kind = "host";
9547
10005
  start(plan, o) {
9548
- const child = spawn2(plan.bin, plan.args, {
10006
+ const child = spawn3(plan.bin, plan.args, {
9549
10007
  cwd: plan.cwd,
9550
10008
  env: o.env,
9551
10009
  stdio: ["pipe", o.streamFd, "inherit"],
@@ -9632,17 +10090,17 @@ var SandboxRunner = class {
9632
10090
  const harness = cfg.agentBin ?? basename7(plan.bin);
9633
10091
  const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
9634
10092
  if (viaUpload) {
9635
- const tmp = mkdtempSync(join22(tmpdir2(), "crew-prompt-"));
10093
+ const tmp = mkdtempSync(join23(tmpdir3(), "crew-prompt-"));
9636
10094
  try {
9637
- const file = join22(tmp, "prompt.txt");
9638
- writeFileSync13(file, plan.prompt, { mode: 384 });
10095
+ const file = join23(tmp, "prompt.txt");
10096
+ writeFileSync14(file, plan.prompt, { mode: 384 });
9639
10097
  await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
9640
10098
  } finally {
9641
- rmSync12(tmp, { recursive: true, force: true });
10099
+ rmSync13(tmp, { recursive: true, force: true });
9642
10100
  }
9643
10101
  }
9644
10102
  const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
9645
- const child = spawn2(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
10103
+ const child = spawn3(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
9646
10104
  env: process.env,
9647
10105
  stdio: ["pipe", o.streamFd, "inherit"],
9648
10106
  detached: true
@@ -9682,7 +10140,7 @@ function describeExecError(e) {
9682
10140
  }
9683
10141
  function gitOrigin(cwd) {
9684
10142
  try {
9685
- return execFileSync5("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
10143
+ return execFileSync6("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9686
10144
  } catch {
9687
10145
  return null;
9688
10146
  }
@@ -9693,16 +10151,16 @@ function runnerFor(kind, sandbox) {
9693
10151
  function runnerOfSidecar(sc) {
9694
10152
  return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
9695
10153
  }
9696
- function checkSandboxRunner(cfg = {}, run2 = (b, a) => execFileSync5(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
10154
+ function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync6(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
9697
10155
  const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
9698
10156
  let version;
9699
10157
  try {
9700
- version = run2(bin, ["--version"]).trim();
10158
+ version = run3(bin, ["--version"]).trim();
9701
10159
  } catch (e) {
9702
10160
  return { ok: false, detail: `${bin} is not installed or not on the scheduler PATH (${e.message.split("\n")[0]})` };
9703
10161
  }
9704
10162
  try {
9705
- run2(bin, ["status"]);
10163
+ run3(bin, ["status"]);
9706
10164
  } catch (e) {
9707
10165
  const err3 = e;
9708
10166
  return {
@@ -9740,7 +10198,7 @@ var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
9740
10198
  function streamShowsAllowanceRefusal(streamPath) {
9741
10199
  let raw;
9742
10200
  try {
9743
- raw = readFileSync19(streamPath, "utf8");
10201
+ raw = readFileSync20(streamPath, "utf8");
9744
10202
  } catch {
9745
10203
  return false;
9746
10204
  }
@@ -9763,13 +10221,13 @@ function streamShowsAllowanceRefusal(streamPath) {
9763
10221
  }
9764
10222
  return false;
9765
10223
  }
9766
- var digestPathFor = (stateDir, routeLabel, role, runId) => join23(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
10224
+ var digestPathFor = (stateDir, routeLabel, role, runId) => join24(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9767
10225
  function planAgentRun(o) {
9768
- const briefPath = join23(o.route.promptsDir, "personas", `lane-${o.role}.md`);
10226
+ const briefPath = join24(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9769
10227
  if (!existsSync21(briefPath)) {
9770
10228
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
9771
10229
  }
9772
- const commonPath = join23(o.route.promptsDir, "personas", "common.md");
10230
+ const commonPath = join24(o.route.promptsDir, "personas", "common.md");
9773
10231
  const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync21(commonPath);
9774
10232
  const now = o.now ?? (() => Date.now());
9775
10233
  const routeLabel = routeSlug(o.route.route);
@@ -9779,10 +10237,10 @@ function planAgentRun(o) {
9779
10237
  let ageSeconds;
9780
10238
  if (existsSync21(digestPath2)) {
9781
10239
  ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
9782
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync19(digestPath2, "utf8");
10240
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync20(digestPath2, "utf8");
9783
10241
  }
9784
- const common = usesCommon ? readFileSync19(commonPath, "utf8") : "";
9785
- const brief = readFileSync19(briefPath, "utf8");
10242
+ const common = usesCommon ? readFileSync20(commonPath, "utf8") : "";
10243
+ const brief = readFileSync20(briefPath, "utf8");
9786
10244
  const effectiveCommon = o.divergedPrompt ? "" : common;
9787
10245
  const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
9788
10246
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -9803,8 +10261,8 @@ function planAgentRun(o) {
9803
10261
  brief: expanded.text,
9804
10262
  digest
9805
10263
  });
9806
- const promptSha = createHash4("sha256").update(expanded.text).digest("hex");
9807
- const streamsDir = join23(o.stateDir, "streams");
10264
+ const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
10265
+ const streamsDir = join24(o.stateDir, "streams");
9808
10266
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
9809
10267
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
9810
10268
  const { model } = seat;
@@ -9890,8 +10348,8 @@ function planAgentRun(o) {
9890
10348
  digestAttached: digest !== void 0,
9891
10349
  digestAgeSeconds: ageSeconds,
9892
10350
  model,
9893
- streamPath: join23(streamsDir, `${base}.jsonl`),
9894
- eventsPath: join23(streamsDir, `${base}.events.jsonl`),
10351
+ streamPath: join24(streamsDir, `${base}.jsonl`),
10352
+ eventsPath: join24(streamsDir, `${base}.events.jsonl`),
9895
10353
  ticket: o.ticket,
9896
10354
  ticketRecordId: o.ticketRecordId ?? null,
9897
10355
  repo: o.repo ?? null,
@@ -9922,7 +10380,7 @@ function describePlan(p) {
9922
10380
  var pidPathFor = (streamPath) => `${streamPath}.pid`;
9923
10381
  function readRunSidecar(streamPath) {
9924
10382
  try {
9925
- const sc = JSON.parse(readFileSync19(pidPathFor(streamPath), "utf8"));
10383
+ const sc = JSON.parse(readFileSync20(pidPathFor(streamPath), "utf8"));
9926
10384
  return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
9927
10385
  } catch {
9928
10386
  return null;
@@ -9969,7 +10427,7 @@ async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMi
9969
10427
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
9970
10428
  function openSink(path) {
9971
10429
  try {
9972
- mkdirSync12(dirname8(path), { recursive: true });
10430
+ mkdirSync13(dirname8(path), { recursive: true });
9973
10431
  return createWriteStream(path, { flags: "a" });
9974
10432
  } catch {
9975
10433
  return void 0;
@@ -10151,7 +10609,7 @@ var RunObserver = class {
10151
10609
  };
10152
10610
  function writeRunMeta(id, startedAt, liveTicket, extra) {
10153
10611
  try {
10154
- writeFileSync14(`${id.streamPath}.meta.json`, JSON.stringify({
10612
+ writeFileSync15(`${id.streamPath}.meta.json`, JSON.stringify({
10155
10613
  route: id.route,
10156
10614
  role: id.role,
10157
10615
  hint: id.ticket,
@@ -10311,7 +10769,7 @@ function spawnAgent(plan, emit, hintMoved) {
10311
10769
  const id = identityOf(plan, adapter.harness);
10312
10770
  let streamFd;
10313
10771
  try {
10314
- mkdirSync12(dirname8(plan.streamPath), { recursive: true });
10772
+ mkdirSync13(dirname8(plan.streamPath), { recursive: true });
10315
10773
  streamFd = openSync3(plan.streamPath, "a");
10316
10774
  } catch (e) {
10317
10775
  void endSink(eventsSink);
@@ -10330,9 +10788,9 @@ function spawnAgent(plan, emit, hintMoved) {
10330
10788
  }
10331
10789
  if (begun instanceof Promise) {
10332
10790
  return begun.then(
10333
- (run2) => {
10791
+ (run3) => {
10334
10792
  closeSync3(streamFd);
10335
- return supervise(run2);
10793
+ return supervise(run3);
10336
10794
  },
10337
10795
  (e) => {
10338
10796
  closeSync3(streamFd);
@@ -10369,7 +10827,7 @@ function spawnAgent(plan, emit, hintMoved) {
10369
10827
  promptSha: plan.promptSha
10370
10828
  };
10371
10829
  try {
10372
- writeFileSync14(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
10830
+ writeFileSync15(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
10373
10831
  } catch {
10374
10832
  }
10375
10833
  }
@@ -10582,9 +11040,9 @@ async function mintMissingSeatKeys(route2, o) {
10582
11040
  out.push({ role, action: "no_agent" });
10583
11041
  continue;
10584
11042
  }
10585
- const slug = slugFor(route2, agentId);
11043
+ const slug2 = slugFor(route2, agentId);
10586
11044
  try {
10587
- if ((await store.get(host, slug))?.apiKey) {
11045
+ if ((await store.get(host, slug2))?.apiKey) {
10588
11046
  out.push({ role, action: "already_stored" });
10589
11047
  continue;
10590
11048
  }
@@ -10622,7 +11080,7 @@ async function mintMissingSeatKeys(route2, o) {
10622
11080
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
10623
11081
  ...body.id ? { keyId: body.id } : {}
10624
11082
  };
10625
- await store.set(host, slug, session);
11083
+ await store.set(host, slug2, session);
10626
11084
  } catch (e) {
10627
11085
  let revoked = false;
10628
11086
  if (body.id) {
@@ -10686,9 +11144,9 @@ async function revokeSeatKeys(route2, o) {
10686
11144
  out.push({ role, action: "no_agent" });
10687
11145
  continue;
10688
11146
  }
10689
- const slug = slugFor(route2, agentId);
11147
+ const slug2 = slugFor(route2, agentId);
10690
11148
  try {
10691
- const session = await store.get(host, slug);
11149
+ const session = await store.get(host, slug2);
10692
11150
  if (!session?.apiKey) {
10693
11151
  out.push({ role, action: "not_stored" });
10694
11152
  continue;
@@ -10709,7 +11167,7 @@ async function revokeSeatKeys(route2, o) {
10709
11167
  out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
10710
11168
  continue;
10711
11169
  }
10712
- await store.clear(host, slug);
11170
+ await store.clear(host, slug2);
10713
11171
  out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
10714
11172
  } catch (e) {
10715
11173
  out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
@@ -10761,7 +11219,7 @@ var realDeps = {
10761
11219
  resolve7(r);
10762
11220
  }
10763
11221
  };
10764
- const child = spawn4(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
11222
+ const child = spawn5(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
10765
11223
  const killGroup = () => {
10766
11224
  try {
10767
11225
  if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
@@ -10859,17 +11317,17 @@ var REMEDY = {
10859
11317
  timeout: "the harness did not answer in time \u2014 run it by hand to see where it stalls",
10860
11318
  "wrong-answer": "the agent answered but did not name the workspace \u2014 run it by hand to see what it said"
10861
11319
  };
10862
- function classify(spec, run2, seen) {
11320
+ function classify(spec, run3, seen) {
10863
11321
  const bin = spec.launcher ?? spec.bin;
10864
11322
  const fail = (failure, what) => ({ failure, detail: `${what} \u2014 ${REMEDY[failure]}` });
10865
- if (run2.spawnError) {
10866
- if (run2.spawnError.code === "ENOENT") return fail("binary-not-found", `\`${bin}\` is not on the scheduler PATH`);
10867
- return fail("binary-not-found", `could not start \`${bin}\` (${run2.spawnError.message})`);
11323
+ if (run3.spawnError) {
11324
+ if (run3.spawnError.code === "ENOENT") return fail("binary-not-found", `\`${bin}\` is not on the scheduler PATH`);
11325
+ return fail("binary-not-found", `could not start \`${bin}\` (${run3.spawnError.message})`);
10868
11326
  }
10869
- if (run2.timedOut) return fail("timeout", "no answer within the time limit");
11327
+ if (run3.timedOut) return fail("timeout", "no answer within the time limit");
10870
11328
  const text2 = `${seen.toolOutput}
10871
- ${run2.stderr}`;
10872
- if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run2.stderr)) {
11329
+ ${run3.stderr}`;
11330
+ if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run3.stderr)) {
10873
11331
  return fail("key-refused", "the tracker returned 401/403 to the MCP call");
10874
11332
  }
10875
11333
  if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text2}
@@ -10877,8 +11335,8 @@ ${seen.texts.join("\n")}`)) {
10877
11335
  return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
10878
11336
  }
10879
11337
  const answered = seen.texts.length > 0 || seen.finalText;
10880
- if (!answered || LOGIN.test(run2.stderr) || LOGIN.test(seen.finalText ?? "")) {
10881
- return fail("login-missing", `\`${bin}\` exited${run2.code !== null ? ` (${run2.code})` : ""} before answering; the vendor login is probably missing`);
11338
+ if (!answered || LOGIN.test(run3.stderr) || LOGIN.test(seen.finalText ?? "")) {
11339
+ return fail("login-missing", `\`${bin}\` exited${run3.code !== null ? ` (${run3.code})` : ""} before answering; the vendor login is probably missing`);
10882
11340
  }
10883
11341
  return fail("wrong-answer", `the agent replied "${(seen.finalText ?? seen.texts.at(-1) ?? "").trim().slice(0, 80)}"`);
10884
11342
  }
@@ -10906,9 +11364,9 @@ async function runSelftest(i, deps = realDeps) {
10906
11364
  ...adapter.runEnv(spec),
10907
11365
  [KEY_VAR]: i.apiKey
10908
11366
  };
10909
- const cwd = mkdtempSync2(join24(tmpdir3(), "crew-selftest-"));
11367
+ const cwd = mkdtempSync2(join25(tmpdir4(), "crew-selftest-"));
10910
11368
  try {
10911
- const run2 = await deps.run({
11369
+ const run3 = await deps.run({
10912
11370
  bin: invocation.bin,
10913
11371
  args: invocation.args,
10914
11372
  cwd,
@@ -10917,18 +11375,18 @@ async function runSelftest(i, deps = realDeps) {
10917
11375
  `,
10918
11376
  timeoutMs: i.timeoutMs ?? SELFTEST_TIMEOUT_MS
10919
11377
  });
10920
- const seen = observe(spec, run2.stdout);
11378
+ const seen = observe(spec, run3.stdout);
10921
11379
  const answer = [seen.finalText ?? "", ...seen.texts].join("\n");
10922
11380
  const usage2 = { ...seen.tokens !== void 0 ? { tokens: seen.tokens } : {}, ...seen.costUsd !== void 0 ? { costUsd: seen.costUsd } : {} };
10923
- if (!run2.spawnError && !run2.timedOut && answer.toLowerCase().includes(i.workspaceName.toLowerCase())) {
11381
+ if (!run3.spawnError && !run3.timedOut && answer.toLowerCase().includes(i.workspaceName.toLowerCase())) {
10924
11382
  const auth = spec.harness === "claude" ? ` on ${describeClaudeAuth(spec)}` : "";
10925
11383
  return finish({ ok: true, workspace: i.workspaceName, detail: `read workspace "${i.workspaceName}"${auth}`, ...usage2 });
10926
11384
  }
10927
- const { failure, detail } = classify(spec, run2, seen);
11385
+ const { failure, detail } = classify(spec, run3, seen);
10928
11386
  return finish({ ok: false, failure, detail, ...usage2 });
10929
11387
  } finally {
10930
11388
  try {
10931
- rmSync13(cwd, { recursive: true, force: true });
11389
+ rmSync14(cwd, { recursive: true, force: true });
10932
11390
  } catch {
10933
11391
  }
10934
11392
  }
@@ -10939,11 +11397,11 @@ function formatResult(r) {
10939
11397
  const secs = `${(r.ms / 1e3).toFixed(1)}s`;
10940
11398
  return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
10941
11399
  }
10942
- var fileFor2 = (stateDir) => join24(stateDir, "selftest.json");
11400
+ var fileFor2 = (stateDir) => join25(stateDir, "selftest.json");
10943
11401
  function readSelftest(stateDir) {
10944
11402
  try {
10945
11403
  const p = fileFor2(stateDir);
10946
- return existsSync22(p) ? JSON.parse(readFileSync20(p, "utf8")) : {};
11404
+ return existsSync22(p) ? JSON.parse(readFileSync21(p, "utf8")) : {};
10947
11405
  } catch {
10948
11406
  return {};
10949
11407
  }
@@ -10951,8 +11409,8 @@ function readSelftest(stateDir) {
10951
11409
  function recordSelftest(stateDir, results) {
10952
11410
  const all = readSelftest(stateDir);
10953
11411
  for (const r of results) all[r.toolchain] = r;
10954
- mkdirSync13(stateDir, { recursive: true });
10955
- writeFileSync15(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
11412
+ mkdirSync14(stateDir, { recursive: true });
11413
+ writeFileSync16(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10956
11414
  `);
10957
11415
  }
10958
11416
  function doctorSelftestLines(stored, toolchains) {
@@ -10964,7 +11422,7 @@ function doctorSelftestLines(stored, toolchains) {
10964
11422
  }
10965
11423
 
10966
11424
  // src/cli.ts
10967
- import { resolve as resolve6, dirname as dirname25, join as join48, basename as basename15 } from "path";
11425
+ import { resolve as resolve6, dirname as dirname25, join as join49, basename as basename15 } from "path";
10968
11426
  import { fileURLToPath } from "url";
10969
11427
  import { hostname as hostname4, homedir as homedir11 } from "os";
10970
11428
  import { spawnSync as spawnSync10 } from "child_process";
@@ -11161,14 +11619,14 @@ function rosterMarkdown(roster, meId) {
11161
11619
  }
11162
11620
 
11163
11621
  // src/connect-resume.ts
11164
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
11165
- import { dirname as dirname9, join as join25 } from "path";
11622
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync22, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
11623
+ import { dirname as dirname9, join as join26 } from "path";
11166
11624
  function lastSessionFile(stateDir) {
11167
- return join25(stateDir, "connect-session.json");
11625
+ return join26(stateDir, "connect-session.json");
11168
11626
  }
11169
11627
  function readPointers(stateDir) {
11170
11628
  try {
11171
- const parsed = JSON.parse(readFileSync21(lastSessionFile(stateDir), "utf8"));
11629
+ const parsed = JSON.parse(readFileSync22(lastSessionFile(stateDir), "utf8"));
11172
11630
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
11173
11631
  const out = {};
11174
11632
  for (const [host, v] of Object.entries(parsed)) {
@@ -11182,8 +11640,8 @@ function readPointers(stateDir) {
11182
11640
  }
11183
11641
  function writePointers(stateDir, pointers) {
11184
11642
  const file = lastSessionFile(stateDir);
11185
- mkdirSync14(dirname9(file), { recursive: true });
11186
- writeFileSync16(file, `${JSON.stringify(pointers, null, 2)}
11643
+ mkdirSync15(dirname9(file), { recursive: true });
11644
+ writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
11187
11645
  `, { mode: 384 });
11188
11646
  }
11189
11647
  function rememberSession(stateDir, host, workspaceSlug) {
@@ -11200,7 +11658,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
11200
11658
  const { [host]: _old, ...rest } = all;
11201
11659
  const next = left.length ? { ...rest, [host]: left } : rest;
11202
11660
  if (Object.keys(next).length) writePointers(stateDir, next);
11203
- else rmSync14(lastSessionFile(stateDir), { force: true });
11661
+ else rmSync15(lastSessionFile(stateDir), { force: true });
11204
11662
  } catch {
11205
11663
  }
11206
11664
  }
@@ -11240,7 +11698,7 @@ function forgetSession(stateDir, host) {
11240
11698
  try {
11241
11699
  const { [host]: _gone, ...rest } = readPointers(stateDir);
11242
11700
  if (Object.keys(rest).length) writePointers(stateDir, rest);
11243
- else rmSync14(lastSessionFile(stateDir), { force: true });
11701
+ else rmSync15(lastSessionFile(stateDir), { force: true });
11244
11702
  } catch {
11245
11703
  }
11246
11704
  }
@@ -11268,15 +11726,15 @@ async function signOutSessions(o) {
11268
11726
  }
11269
11727
 
11270
11728
  // src/reset.ts
11271
- import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync9, rmSync as rmSync15, statSync as statSync8, writeFileSync as writeFileSync17 } from "fs";
11272
- import { dirname as dirname10, join as join26 } from "path";
11729
+ import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync9, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
11730
+ import { dirname as dirname10, join as join27 } from "path";
11273
11731
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
11274
11732
  function clonedCheckoutsFile(stateDir) {
11275
- return join26(stateDir, "cloned-checkouts.json");
11733
+ return join27(stateDir, "cloned-checkouts.json");
11276
11734
  }
11277
11735
  function readClonedCheckouts(stateDir) {
11278
11736
  try {
11279
- const parsed = JSON.parse(readFileSync22(clonedCheckoutsFile(stateDir), "utf8"));
11737
+ const parsed = JSON.parse(readFileSync23(clonedCheckoutsFile(stateDir), "utf8"));
11280
11738
  return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
11281
11739
  } catch {
11282
11740
  return [];
@@ -11286,8 +11744,8 @@ function recordClonedCheckout(stateDir, dir) {
11286
11744
  const known = readClonedCheckouts(stateDir);
11287
11745
  if (known.includes(dir)) return;
11288
11746
  const file = clonedCheckoutsFile(stateDir);
11289
- mkdirSync15(dirname10(file), { recursive: true });
11290
- writeFileSync17(file, `${JSON.stringify([...known, dir], null, 2)}
11747
+ mkdirSync16(dirname10(file), { recursive: true });
11748
+ writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
11291
11749
  `);
11292
11750
  }
11293
11751
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -11342,7 +11800,7 @@ async function runReset(o, fx) {
11342
11800
  if (o.beforeState) steps.push(...await o.beforeState());
11343
11801
  steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
11344
11802
  if (existsSync23(o.configFile)) {
11345
- if (!o.dryRun) rmSync15(o.configFile, { force: true });
11803
+ if (!o.dryRun) rmSync16(o.configFile, { force: true });
11346
11804
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
11347
11805
  } else {
11348
11806
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -11358,21 +11816,21 @@ function removeCheckout(dir, dryRun, fx) {
11358
11816
  if (linkedWorktrees.length) {
11359
11817
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
11360
11818
  }
11361
- if (!dryRun) rmSync15(dir, { recursive: true, force: true });
11819
+ if (!dryRun) rmSync16(dir, { recursive: true, force: true });
11362
11820
  return { step: "checkout", target: dir, action: mark(dryRun) };
11363
11821
  }
11364
11822
  function removeState(stateDir, dryRun) {
11365
11823
  const out = [];
11366
- const targets = [join26(stateDir, "resolved"), join26(stateDir, "keys"), clonedCheckoutsFile(stateDir), join26(stateDir, "connect-session.json"), join26(stateDir, "reload-requested"), join26(stateDir, "daemon.started")];
11824
+ const targets = [join27(stateDir, "resolved"), join27(stateDir, "keys"), clonedCheckoutsFile(stateDir), join27(stateDir, "connect-session.json"), join27(stateDir, "reload-requested"), join27(stateDir, "daemon.started")];
11367
11825
  try {
11368
11826
  for (const name of readdirSync9(stateDir)) {
11369
- if (name.startsWith(".") && statSync8(join26(stateDir, name)).isFile()) targets.push(join26(stateDir, name));
11827
+ if (name.startsWith(".") && statSync8(join27(stateDir, name)).isFile()) targets.push(join27(stateDir, name));
11370
11828
  }
11371
11829
  } catch {
11372
11830
  }
11373
11831
  for (const t of targets) {
11374
11832
  if (!existsSync23(t)) continue;
11375
- if (!dryRun) rmSync15(t, { recursive: true, force: true });
11833
+ if (!dryRun) rmSync16(t, { recursive: true, force: true });
11376
11834
  out.push({ step: "state", target: t, action: mark(dryRun) });
11377
11835
  }
11378
11836
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -11386,7 +11844,7 @@ function wipeStateDir(stateDir, dryRun) {
11386
11844
  return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
11387
11845
  }
11388
11846
  if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
11389
- if (!dryRun) for (const n of names) rmSync15(join26(stateDir, n), { recursive: true, force: true });
11847
+ if (!dryRun) for (const n of names) rmSync16(join27(stateDir, n), { recursive: true, force: true });
11390
11848
  return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
11391
11849
  }
11392
11850
  function describeResetStep(s) {
@@ -11410,21 +11868,21 @@ async function runDeprovision(o, fx, deps) {
11410
11868
  };
11411
11869
  const afterScheduler = async () => {
11412
11870
  const steps2 = [];
11413
- for (const run2 of deps.liveRuns()) {
11414
- const target = `${run2.role} run on ${run2.route} (pid ${run2.pid})`;
11871
+ for (const run3 of deps.liveRuns()) {
11872
+ const target = `${run3.role} run on ${run3.route} (pid ${run3.pid})`;
11415
11873
  if (o.dryRun) {
11416
11874
  steps2.push({ step: "run", target, action: "would-remove" });
11417
11875
  continue;
11418
11876
  }
11419
11877
  try {
11420
- await deps.killRun(run2);
11421
- deps.recordKill(run2);
11878
+ await deps.killRun(run3);
11879
+ deps.recordKill(run3);
11422
11880
  } catch (e) {
11423
11881
  steps2.push({ step: "run", target, action: "failed", detail: e.message });
11424
11882
  continue;
11425
11883
  }
11426
11884
  try {
11427
- await deps.handBack(run2);
11885
+ await deps.handBack(run3);
11428
11886
  steps2.push({ step: "run", target, action: "removed", detail: "killed" });
11429
11887
  } catch (e) {
11430
11888
  steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
@@ -11471,8 +11929,8 @@ import { spawnSync as spawnSync6 } from "child_process";
11471
11929
  import { existsSync as existsSync26, readdirSync as readdirSync12 } from "fs";
11472
11930
 
11473
11931
  // src/interrupted.ts
11474
- import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
11475
- import { join as join27 } from "path";
11932
+ import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
11933
+ import { join as join28 } from "path";
11476
11934
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
11477
11935
  function interruptedComment(b) {
11478
11936
  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.";
@@ -11493,19 +11951,19 @@ function runFinished(streamPath) {
11493
11951
  const path = `${streamPath}.meta.json`;
11494
11952
  if (!existsSync24(path)) return false;
11495
11953
  try {
11496
- return JSON.parse(readFileSync23(path, "utf8")).running !== true;
11954
+ return JSON.parse(readFileSync24(path, "utf8")).running !== true;
11497
11955
  } catch {
11498
11956
  return true;
11499
11957
  }
11500
11958
  }
11501
11959
  function orphanedStreams(o) {
11502
- const dir = join27(o.stateDir, "streams");
11960
+ const dir = join28(o.stateDir, "streams");
11503
11961
  if (!existsSync24(dir)) return [];
11504
11962
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
11505
11963
  const out = [];
11506
11964
  for (const name of readdirSync10(dir)) {
11507
11965
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
11508
- const streamPath = join27(dir, name);
11966
+ const streamPath = join28(dir, name);
11509
11967
  if (runFinished(streamPath)) continue;
11510
11968
  const sidecar = readRunSidecar(streamPath);
11511
11969
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -11559,14 +12017,14 @@ async function reconcileRuns(o) {
11559
12017
 
11560
12018
  // src/live-runs.ts
11561
12019
  import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
11562
- import { join as join28 } from "path";
12020
+ import { join as join29 } from "path";
11563
12021
  function liveRuns(stateDir, isAlive = runIsAlive) {
11564
- const dir = join28(stateDir, "streams");
12022
+ const dir = join29(stateDir, "streams");
11565
12023
  if (!existsSync25(dir)) return [];
11566
12024
  const out = [];
11567
12025
  for (const name of readdirSync11(dir)) {
11568
12026
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
11569
- const streamPath = join28(dir, name);
12027
+ const streamPath = join29(dir, name);
11570
12028
  if (runFinished(streamPath)) continue;
11571
12029
  const sidecar = readRunSidecar(streamPath);
11572
12030
  if (!sidecar || !isAlive(sidecar)) continue;
@@ -11580,8 +12038,8 @@ function siblingRuns(runs, ownTicketRecordId) {
11580
12038
  async function settleRunEnd(o) {
11581
12039
  const { tracker, emit } = o;
11582
12040
  const all = o.runs ?? liveRuns(o.stateDir);
11583
- const slug = routeSlug(o.route.route);
11584
- const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug), o.ownTicketRecordId);
12041
+ const slug2 = routeSlug(o.route.route);
12042
+ const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug2), o.ownTicketRecordId);
11585
12043
  const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
11586
12044
  const newest = (rs) => rs[rs.length - 1];
11587
12045
  if (o.memberId) {
@@ -11603,234 +12061,30 @@ async function settleRunEnd(o) {
11603
12061
  emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
11604
12062
  }
11605
12063
  }
11606
- function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
12064
+ function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
11607
12065
  try {
11608
12066
  return worktreeForNumber(d, n)?.path ?? null;
11609
12067
  } catch {
11610
12068
  return null;
11611
12069
  }
11612
12070
  }) {
11613
- const ownRoute = run2.route === routeSlug(route2.route);
11614
- const repoDir = run2.repo ? reposOf(route2).find((t) => t.name === run2.repo)?.dir : void 0;
11615
- const num = run2.ticket?.replace(/^\D+/, "") ?? "";
12071
+ const ownRoute = run3.route === routeSlug(route2.route);
12072
+ const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
12073
+ const num = run3.ticket?.replace(/^\D+/, "") ?? "";
11616
12074
  return {
11617
- role: run2.role,
11618
- seat: ownRoute ? seatIds[run2.role] ?? null : null,
11619
- ticket: run2.ticket,
11620
- ticketRecordId: run2.ticketRecordId,
11621
- repo: run2.repo ?? null,
12075
+ role: run3.role,
12076
+ seat: ownRoute ? seatIds[run3.role] ?? null : null,
12077
+ ticket: run3.ticket,
12078
+ ticketRecordId: run3.ticketRecordId,
12079
+ repo: run3.repo ?? null,
11622
12080
  worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
11623
- stream: run2.streamPath,
11624
- pid: run2.pid,
11625
- startedAt: run2.spawnedAt,
11626
- deadline: run2.deadline ?? null
12081
+ stream: run3.streamPath,
12082
+ pid: run3.pid,
12083
+ startedAt: run3.spawnedAt,
12084
+ deadline: run3.deadline ?? null
11627
12085
  };
11628
12086
  }
11629
12087
 
11630
- // src/passenger-containers.ts
11631
- import { execFileSync as execFileSync6 } from "child_process";
11632
- import { createHash as createHash5 } from "crypto";
11633
- var realExec = (cmd, args, opts) => {
11634
- try {
11635
- return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
11636
- } catch (e) {
11637
- const err3 = e;
11638
- const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
11639
- throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
11640
- }
11641
- };
11642
- var DOCKER_INFO_TIMEOUT_MS = 1e4;
11643
- function dockerAvailable(exec = realExec) {
11644
- try {
11645
- exec("docker", ["info"], { timeoutMs: DOCKER_INFO_TIMEOUT_MS });
11646
- return true;
11647
- } catch {
11648
- return false;
11649
- }
11650
- }
11651
- var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
11652
- function passengerImageRef(cfg) {
11653
- return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
11654
- }
11655
- function imagePresent(image, exec = realExec) {
11656
- try {
11657
- exec("docker", ["image", "inspect", image]);
11658
- return true;
11659
- } catch {
11660
- return false;
11661
- }
11662
- }
11663
- function ensureImage(image, exec = realExec) {
11664
- if (!imagePresent(image, exec)) exec("docker", ["pull", image]);
11665
- }
11666
- function containerNameFor(workspaceId) {
11667
- return `crew-passenger-${workspaceId}`;
11668
- }
11669
- function portFor(workspaceId, base = 28765, span = 1e3) {
11670
- const digest = createHash5("sha256").update(workspaceId).digest();
11671
- return base + digest.readUInt32BE(0) % span;
11672
- }
11673
- function mountsDigest(mounts, image) {
11674
- const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
11675
- return createHash5("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
11676
- }
11677
- function planContainers(cfg) {
11678
- const byWorkspace = /* @__PURE__ */ new Map();
11679
- const image = passengerImageRef(cfg);
11680
- for (const route2 of cfg.routes) {
11681
- if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
11682
- const workspaceId = route2.resolved.workspaceId;
11683
- const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
11684
- for (const target of passengerCheckoutTargets(route2)) {
11685
- mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
11686
- }
11687
- byWorkspace.set(workspaceId, mounts);
11688
- }
11689
- return [...byWorkspace.entries()].filter(([, mounts]) => mounts.size > 0).map(([workspaceId, mounts]) => {
11690
- const mountList = [...mounts.values()];
11691
- return {
11692
- workspaceId,
11693
- containerName: containerNameFor(workspaceId),
11694
- port: portFor(workspaceId),
11695
- image,
11696
- mounts: mountList,
11697
- mountsHash: mountsDigest(mountList, image)
11698
- };
11699
- });
11700
- }
11701
- function listPassengerContainers(exec = realExec) {
11702
- const out = exec("docker", ["ps", "-a", "--filter", "name=crew-passenger-", "--format", "{{.Names}} {{.State}}"]);
11703
- return out.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
11704
- const [name, state] = line.split(" ");
11705
- return { name, running: state === "running" };
11706
- });
11707
- }
11708
- function currentMountsHash(name, exec) {
11709
- try {
11710
- const out = exec("docker", ["inspect", "--format", '{{ index .Config.Labels "crew.mounts.hash" }}', name]);
11711
- const hash = out.trim();
11712
- return hash || void 0;
11713
- } catch {
11714
- return void 0;
11715
- }
11716
- }
11717
- function stopContainer(name, exec = realExec) {
11718
- try {
11719
- exec("docker", ["rm", "-f", name]);
11720
- } catch {
11721
- }
11722
- }
11723
- function startContainer(plan, tablationApiBaseUrl, sync, exec = realExec) {
11724
- const args = [
11725
- "run",
11726
- "-d",
11727
- "--name",
11728
- plan.containerName,
11729
- "--restart",
11730
- "unless-stopped",
11731
- "-p",
11732
- `${plan.port}:8765`,
11733
- "-e",
11734
- `PASSENGER_MCP_WORKSPACE_ID=${plan.workspaceId}`,
11735
- "-e",
11736
- `TABLATION_API_BASE_URL=${tablationApiBaseUrl}`,
11737
- "--label",
11738
- `crew.mounts.hash=${plan.mountsHash}`
11739
- ];
11740
- if (sync) {
11741
- args.push(
11742
- "--add-host",
11743
- "host.docker.internal:host-gateway",
11744
- "-e",
11745
- `PASSENGER_SYNC_URL=http://${sync.host}:${sync.port}/sync`,
11746
- "-e",
11747
- `PASSENGER_SYNC_SECRET=${sync.secret}`,
11748
- "-e",
11749
- `PASSENGER_MCP_SECRET=${sync.secret}`
11750
- );
11751
- }
11752
- for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
11753
- args.push(plan.image);
11754
- exec("docker", args);
11755
- }
11756
- function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
11757
- const plans = planContainers(cfg);
11758
- const plannedByName = new Map(plans.map((p) => [p.containerName, p]));
11759
- const existing = listPassengerContainers(exec);
11760
- const existingByName = new Map(existing.map((c) => [c.name, c]));
11761
- const started = [];
11762
- const recreated = [];
11763
- const stopped = [];
11764
- const ensured = /* @__PURE__ */ new Set();
11765
- const start = (plan, sync) => {
11766
- if (!ensured.has(plan.image)) {
11767
- ensureImage(plan.image, exec);
11768
- ensured.add(plan.image);
11769
- }
11770
- startContainer(plan, tablationApiBaseUrl, sync, exec);
11771
- };
11772
- for (const plan of plans) {
11773
- const sync = syncEndpoints.get(plan.workspaceId);
11774
- const current = existingByName.get(plan.containerName);
11775
- if (!current) {
11776
- start(plan, sync);
11777
- started.push(plan.containerName);
11778
- } else if (!current.running) {
11779
- stopContainer(plan.containerName, exec);
11780
- start(plan, sync);
11781
- started.push(plan.containerName);
11782
- } else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
11783
- stopContainer(plan.containerName, exec);
11784
- start(plan, sync);
11785
- recreated.push(plan.containerName);
11786
- }
11787
- }
11788
- for (const { name } of existing) {
11789
- if (!plannedByName.has(name)) {
11790
- stopContainer(name, exec);
11791
- stopped.push(name);
11792
- }
11793
- }
11794
- return { started, recreated, stopped };
11795
- }
11796
- function passengerCheckoutSyncSpecs(cfg, workspaceId) {
11797
- const specs = [];
11798
- for (const route2 of cfg.routes) {
11799
- if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
11800
- if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
11801
- const routeWorkspaceId = route2.resolved.workspaceId;
11802
- const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
11803
- for (const target of passengerCheckoutTargets(route2)) {
11804
- const ordinary = ordinaryByName.get(target.name);
11805
- const override = route2.repoOverrides[target.name];
11806
- const effective = resolveRepoConfig(
11807
- ordinary ? loadRepoConfig(ordinary.dir) : null,
11808
- { branch: { ...route2.branch, ...override?.branch } },
11809
- ordinary?.dir ?? target.dir
11810
- );
11811
- specs.push({
11812
- name: target.name,
11813
- workspaceId: routeWorkspaceId,
11814
- dir: target.dir,
11815
- remote: target.remote,
11816
- gitRemoteName: effective.branch.remote,
11817
- base: effective.branch.base,
11818
- originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
11819
- cloneScheme: cfg.ship.cloneScheme,
11820
- ...target.forgeHost ? { forgeHost: target.forgeHost } : {}
11821
- });
11822
- }
11823
- }
11824
- return specs;
11825
- }
11826
- function syncAllPassengerCheckouts(cfg) {
11827
- return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
11828
- name: spec.name,
11829
- workspaceId: spec.workspaceId,
11830
- outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
11831
- }));
11832
- }
11833
-
11834
12088
  // src/deprovision-host.ts
11835
12089
  var DOCKER_LABEL_OWNER = "crew.owner=crew";
11836
12090
  var DOCKER_LABEL_CACHE = "crew.kind=cache";
@@ -11862,22 +12116,22 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
11862
12116
  };
11863
12117
  return {
11864
12118
  liveRuns: () => liveRuns(stateDir).map((r) => ({ role: r.role, route: r.route, pid: r.pid, ticketRecordId: r.ticketRecordId ?? void 0, streamPath: r.streamPath })),
11865
- killRun: (run2) => killRunGroup(run2.pid),
11866
- recordKill(run2) {
11867
- emit.emit(`run_killed: ${run2.role} run on ${run2.route} stopped by crew deprovision`, {
12119
+ killRun: (run3) => killRunGroup(run3.pid),
12120
+ recordKill(run3) {
12121
+ emit.emit(`run_killed: ${run3.role} run on ${run3.route} stopped by crew deprovision`, {
11868
12122
  step: "agent",
11869
- role: run2.role,
11870
- data: { event: "run_killed", reason: "deprovision", pid: run2.pid }
12123
+ role: run3.role,
12124
+ data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
11871
12125
  });
11872
12126
  },
11873
- async handBack(run2) {
11874
- const route2 = routeFor(run2.route);
11875
- if (!run2.ticketRecordId || !route2) return;
11876
- const memberId = route2.resolved?.seats[run2.role];
12127
+ async handBack(run3) {
12128
+ const route2 = routeFor(run3.route);
12129
+ if (!run3.ticketRecordId || !route2) return;
12130
+ const memberId = route2.resolved?.seats[run3.role];
11877
12131
  if (!memberId) return;
11878
- const identity = await resolveSeatKey(route2, run2.role, getStore);
12132
+ const identity = await resolveSeatKey(route2, run3.role, getStore);
11879
12133
  const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
11880
- const t = await tracker.ticket(run2.ticketRecordId);
12134
+ const t = await tracker.ticket(run3.ticketRecordId);
11881
12135
  const target = repoTargetFor(route2, t.repo_id);
11882
12136
  const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
11883
12137
  const wt = target && num ? worktreeForNumber(target.dir, num) : null;
@@ -11886,7 +12140,7 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
11886
12140
  if (wt) dirty = status(wt.path).length > 0;
11887
12141
  } catch {
11888
12142
  }
11889
- await tracker.postEvent(run2.ticketRecordId, interruptedComment({
12143
+ await tracker.postEvent(run3.ticketRecordId, interruptedComment({
11890
12144
  branch: wt?.branch ?? null,
11891
12145
  dirty,
11892
12146
  reason: "the host was released and wiped"
@@ -11941,9 +12195,9 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
11941
12195
  }
11942
12196
 
11943
12197
  // src/pool-member.ts
11944
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync24, renameSync as renameSync3, writeFileSync as writeFileSync18 } from "fs";
12198
+ import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
11945
12199
  import { homedir as homedir8 } from "os";
11946
- import { dirname as dirname11, join as join29 } from "path";
12200
+ import { dirname as dirname11, join as join30 } from "path";
11947
12201
  var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
11948
12202
  function backoffMs(attempt) {
11949
12203
  return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
@@ -12132,24 +12386,24 @@ async function runPoolMember(deps) {
12132
12386
  }
12133
12387
  }
12134
12388
  function defaultMemberDir(env = process.env) {
12135
- return env.CREW_POOL_DIR ?? join29(env.XDG_STATE_HOME ?? join29(homedir8(), ".local", "state"), "crew-pool-member");
12389
+ return env.CREW_POOL_DIR ?? join30(env.XDG_STATE_HOME ?? join30(homedir8(), ".local", "state"), "crew-pool-member");
12136
12390
  }
12137
12391
  function fileMemberStore(dir) {
12138
- const file = join29(dir, "member.json");
12392
+ const file = join30(dir, "member.json");
12139
12393
  return {
12140
12394
  read() {
12141
12395
  try {
12142
- return JSON.parse(readFileSync24(file, "utf8"));
12396
+ return JSON.parse(readFileSync25(file, "utf8"));
12143
12397
  } catch {
12144
12398
  return {};
12145
12399
  }
12146
12400
  },
12147
12401
  write(f) {
12148
- mkdirSync16(dirname11(file), { recursive: true, mode: 448 });
12402
+ mkdirSync17(dirname11(file), { recursive: true, mode: 448 });
12149
12403
  const tmp = `${file}.tmp`;
12150
- writeFileSync18(tmp, `${JSON.stringify(f)}
12404
+ writeFileSync19(tmp, `${JSON.stringify(f)}
12151
12405
  `, { mode: 384 });
12152
- renameSync3(tmp, file);
12406
+ renameSync4(tmp, file);
12153
12407
  }
12154
12408
  };
12155
12409
  }
@@ -12204,13 +12458,13 @@ import { hostname as hostname3 } from "os";
12204
12458
 
12205
12459
  // src/machine-id.ts
12206
12460
  import { execFileSync as execFileSync7 } from "child_process";
12207
- import { createHash as createHash6 } from "crypto";
12208
- import { existsSync as existsSync27, mkdirSync as mkdirSync17, readFileSync as readFileSync25, writeFileSync as writeFileSync19 } from "fs";
12209
- import { dirname as dirname12, join as join30 } from "path";
12461
+ import { createHash as createHash7 } from "crypto";
12462
+ import { existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
12463
+ import { dirname as dirname12, join as join31 } from "path";
12210
12464
  function machineIdPathFor(stateDir) {
12211
- return join30(stateDir, "machine-id");
12465
+ return join31(stateDir, "machine-id");
12212
12466
  }
12213
- var sha256 = (s) => createHash6("sha256").update(s).digest("hex");
12467
+ var sha256 = (s) => createHash7("sha256").update(s).digest("hex");
12214
12468
  function readRawMachineId(platform2 = process.platform) {
12215
12469
  try {
12216
12470
  if (platform2 === "darwin") {
@@ -12222,7 +12476,7 @@ function readRawMachineId(platform2 = process.platform) {
12222
12476
  }
12223
12477
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
12224
12478
  if (existsSync27(p)) {
12225
- const id = readFileSync25(p, "utf8").trim();
12479
+ const id = readFileSync26(p, "utf8").trim();
12226
12480
  if (id) return id;
12227
12481
  }
12228
12482
  }
@@ -12234,14 +12488,14 @@ function ensureMachineId(stateDir, opts = {}) {
12234
12488
  if (opts.override?.trim()) return sha256(opts.override.trim());
12235
12489
  const path = machineIdPathFor(stateDir);
12236
12490
  if (existsSync27(path)) {
12237
- const cached = readFileSync25(path, "utf8").trim();
12491
+ const cached = readFileSync26(path, "utf8").trim();
12238
12492
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
12239
12493
  }
12240
12494
  const raw = (opts.read ?? readRawMachineId)();
12241
12495
  if (!raw) return void 0;
12242
12496
  const hashed = sha256(raw);
12243
- mkdirSync17(dirname12(path), { recursive: true });
12244
- writeFileSync19(path, `${hashed}
12497
+ mkdirSync18(dirname12(path), { recursive: true });
12498
+ writeFileSync20(path, `${hashed}
12245
12499
  `, { mode: 384 });
12246
12500
  return hashed;
12247
12501
  }
@@ -12254,7 +12508,7 @@ function matchShipRows(ships, shipName2, machineId) {
12254
12508
 
12255
12509
  // src/provision.ts
12256
12510
  import { spawnSync as spawnSync7 } from "child_process";
12257
- import { existsSync as existsSync28, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
12511
+ import { existsSync as existsSync28, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
12258
12512
  import { hostname as hostname2 } from "os";
12259
12513
  import { parseDocument as parseDocument3 } from "yaml";
12260
12514
 
@@ -12263,7 +12517,7 @@ import { parseDocument as parseDocument2 } from "yaml";
12263
12517
 
12264
12518
  // src/repos-cmd.ts
12265
12519
  import { homedir as homedir9 } from "os";
12266
- import { basename as basename9, join as join31, resolve as resolve3 } from "path";
12520
+ import { basename as basename9, join as join32, resolve as resolve3 } from "path";
12267
12521
  import { isMap, isSeq, parseDocument, stringify } from "yaml";
12268
12522
  var ReposError = class extends Error {
12269
12523
  };
@@ -12541,7 +12795,7 @@ function planRepoClone(i) {
12541
12795
  }
12542
12796
  const ship = doc.get("ship", true);
12543
12797
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
12544
- const target = i.dir ? expandDir(i.dir, i.base) : join31(expandDir(basePath, i.base), i.route.split("/")[0], name);
12798
+ const target = i.dir ? expandDir(i.dir, i.base) : join32(expandDir(basePath, i.base), i.route.split("/")[0], name);
12545
12799
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
12546
12800
  const forgeHost = i.resolved?.repoForgeHosts?.[id];
12547
12801
  return { ...add, remote, target, ...forgeHost ? { forgeHost } : {} };
@@ -12814,13 +13068,13 @@ function realProvisionDeps(crewHome) {
12814
13068
  enableRoute(route2) {
12815
13069
  const file = configFile();
12816
13070
  if (!file) return false;
12817
- const text2 = readFileSync26(file, "utf8");
13071
+ const text2 = readFileSync27(file, "utf8");
12818
13072
  const doc = parseDocument3(text2);
12819
13073
  const routes = doc.get("routes");
12820
13074
  const entry = routes?.items?.find((r) => r.get("route") === route2);
12821
13075
  if (!entry) throw new Error(`route ${route2} is not in ${file}`);
12822
13076
  if (entry.get("enabled") === true) return false;
12823
- writeFileSync20(file, planEnable(text2, route2));
13077
+ writeFileSync21(file, planEnable(text2, route2));
12824
13078
  return true;
12825
13079
  },
12826
13080
  installState() {
@@ -12840,7 +13094,7 @@ function realProvisionDeps(crewHome) {
12840
13094
  continue;
12841
13095
  }
12842
13096
  for (const p of plan.unitPaths) {
12843
- if (!existsSync28(p) || readFileSync26(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
13097
+ if (!existsSync28(p) || readFileSync27(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
12844
13098
  }
12845
13099
  }
12846
13100
  const label = labelFor(crewHome, "run");
@@ -12864,10 +13118,10 @@ function realProvisionDeps(crewHome) {
12864
13118
  setShipSetting(key, value2) {
12865
13119
  const file = configFile();
12866
13120
  if (!file) return false;
12867
- const doc = parseDocument3(readFileSync26(file, "utf8"));
13121
+ const doc = parseDocument3(readFileSync27(file, "utf8"));
12868
13122
  if (doc.getIn(["ship", key]) === value2) return false;
12869
13123
  doc.setIn(["ship", key], value2);
12870
- writeFileSync20(file, doc.toString());
13124
+ writeFileSync21(file, doc.toString());
12871
13125
  return true;
12872
13126
  },
12873
13127
  storedToken(host) {
@@ -12883,12 +13137,12 @@ host=${host}
12883
13137
  }
12884
13138
 
12885
13139
  // src/tunnel.ts
12886
- import { spawn as spawn5 } from "child_process";
12887
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync27, writeFileSync as writeFileSync21, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
13140
+ import { spawn as spawn6 } from "child_process";
13141
+ import { existsSync as existsSync29, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
12888
13142
  import { randomInt } from "crypto";
12889
- import { join as join32 } from "path";
13143
+ import { join as join33 } from "path";
12890
13144
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
12891
- var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
13145
+ var defaultSpawn = (cmd, args) => spawn6(cmd, args, DEFAULT_SPAWN_OPTIONS);
12892
13146
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
12893
13147
  var SLUG_LENGTH = 12;
12894
13148
  function generateTunnelSlug() {
@@ -12900,12 +13154,12 @@ function bindAddrFor(o, path) {
12900
13154
  const base = `workspace-${o.workspaceId}-slug-${o.slug}${path ? `-path-${path}` : ""}-key-${o.apiKey}`;
12901
13155
  return o.mcpSecret && !path ? `${base}-secret-${o.mcpSecret}` : base;
12902
13156
  }
12903
- function relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
13157
+ function relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
12904
13158
  const domain = relayPublicDomain ?? relayHost.replace(/^.*@/, "");
12905
- return `https://${slug}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
13159
+ return `https://${slug2}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
12906
13160
  }
12907
- function publicUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
12908
- return `${relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
13161
+ function publicUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
13162
+ return `${relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
12909
13163
  }
12910
13164
  function sshArgsFor(o) {
12911
13165
  const forwards = [];
@@ -12958,21 +13212,21 @@ function startTunnel(opts) {
12958
13212
  };
12959
13213
  }
12960
13214
  function tunnelStatePath(stateDir, workspaceId) {
12961
- return join32(stateDir, "passengers", `${workspaceId}.json`);
13215
+ return join33(stateDir, "passengers", `${workspaceId}.json`);
12962
13216
  }
12963
13217
  function readPersistedTunnel(stateDir, workspaceId) {
12964
13218
  const path = tunnelStatePath(stateDir, workspaceId);
12965
13219
  if (!existsSync29(path)) return void 0;
12966
13220
  try {
12967
- return JSON.parse(readFileSync27(path, "utf8"));
13221
+ return JSON.parse(readFileSync28(path, "utf8"));
12968
13222
  } catch {
12969
13223
  return void 0;
12970
13224
  }
12971
13225
  }
12972
13226
  function writePersistedTunnel(stateDir, state) {
12973
13227
  const path = tunnelStatePath(stateDir, state.workspaceId);
12974
- mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
12975
- writeFileSync21(path, `${JSON.stringify(state, null, 2)}
13228
+ mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
13229
+ writeFileSync22(path, `${JSON.stringify(state, null, 2)}
12976
13230
  `);
12977
13231
  }
12978
13232
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -13006,8 +13260,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13006
13260
  const controlUnchanged = existing?.controlSocket === deps.controlSocket;
13007
13261
  const currentSecret = mcpSecrets.get(plan.workspaceId);
13008
13262
  const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
13009
- const slug = existing?.slug ?? generateTunnelSlug();
13010
- const slugUnchanged = existing?.liveSlug === slug;
13263
+ const slug2 = existing?.slug ?? generateTunnelSlug();
13264
+ const slugUnchanged = existing?.liveSlug === slug2;
13011
13265
  if (existing && alive(existing.pid) && existing.mountsHash === mountsHash && secretUnchanged && slugUnchanged && controlUnchanged) {
13012
13266
  if (existing.status === "connecting") {
13013
13267
  const settled = { ...existing, status: "connected" };
@@ -13032,7 +13286,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13032
13286
  relayHost,
13033
13287
  relayPort,
13034
13288
  workspaceId: plan.workspaceId,
13035
- slug,
13289
+ slug: slug2,
13036
13290
  apiKey,
13037
13291
  localPort: plan.port,
13038
13292
  controlSocket: deps.controlSocket,
@@ -13052,8 +13306,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13052
13306
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
13053
13307
  status: settledStatus,
13054
13308
  mcpSecret: currentSecret,
13055
- slug,
13056
- liveSlug: slug,
13309
+ slug: slug2,
13310
+ liveSlug: slug2,
13057
13311
  endpointId: existing?.endpointId,
13058
13312
  registeredKey: existing?.registeredKey
13059
13313
  };
@@ -13074,7 +13328,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13074
13328
  }
13075
13329
  }
13076
13330
  function readAllPersistedWorkspaceIds(stateDir) {
13077
- const dir = join32(stateDir, "passengers");
13331
+ const dir = join33(stateDir, "passengers");
13078
13332
  if (!existsSync29(dir)) return [];
13079
13333
  return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
13080
13334
  }
@@ -13157,8 +13411,8 @@ function realPoolMemberDeps(o) {
13157
13411
  if (!cfg?.ship.relayHost) return null;
13158
13412
  for (const r of cfg.routes) {
13159
13413
  const ws = r.resolved?.workspaceId;
13160
- const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
13161
- if (slug) return relayBaseUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
13414
+ const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
13415
+ if (slug2) return relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
13162
13416
  }
13163
13417
  return null;
13164
13418
  },
@@ -13941,8 +14195,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
13941
14195
  }
13942
14196
 
13943
14197
  // src/poll.ts
13944
- import { writeFileSync as writeFileSync22 } from "fs";
13945
- import { join as join33 } from "path";
14198
+ import { writeFileSync as writeFileSync23 } from "fs";
14199
+ import { join as join34 } from "path";
13946
14200
 
13947
14201
  // src/stalled.ts
13948
14202
  var STALLED_AFTER_CYCLES = 30;
@@ -14249,7 +14503,7 @@ async function decideCycle(o) {
14249
14503
  }
14250
14504
  return decision;
14251
14505
  }
14252
- var digestPath = (stateDir, route2, role, runId) => join33(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
14506
+ var digestPath = (stateDir, route2, role, runId) => join34(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
14253
14507
  function otherRunsRemoved(tickets, own) {
14254
14508
  if (!own) return tickets;
14255
14509
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -14327,7 +14581,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
14327
14581
 
14328
14582
  ${body}` : body;
14329
14583
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
14330
- writeFileSync22(path, text2);
14584
+ writeFileSync23(path, text2);
14331
14585
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
14332
14586
  data: { role, bytes: Buffer.byteLength(text2) }
14333
14587
  });
@@ -14435,7 +14689,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
14435
14689
  }
14436
14690
 
14437
14691
  // src/repo-refresh.ts
14438
- import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync23 } from "fs";
14692
+ import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
14439
14693
  import { dirname as dirname14 } from "path";
14440
14694
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
14441
14695
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
@@ -14466,10 +14720,10 @@ function applyRepoRows(resolved, rows) {
14466
14720
  function persistRepoRows(stateDir, routeName, rows) {
14467
14721
  const path = resolvedPathFor(stateDir, routeName);
14468
14722
  if (!existsSync31(path)) return;
14469
- const raw = JSON.parse(readFileSync28(path, "utf8"));
14723
+ const raw = JSON.parse(readFileSync29(path, "utf8"));
14470
14724
  applyRepoRows(raw, rows);
14471
- mkdirSync19(dirname14(path), { recursive: true });
14472
- writeFileSync23(path, `${JSON.stringify(raw, null, 2)}
14725
+ mkdirSync20(dirname14(path), { recursive: true });
14726
+ writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
14473
14727
  `);
14474
14728
  }
14475
14729
  async function refreshUnknownRepos(o) {
@@ -14919,8 +15173,8 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
14919
15173
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
14920
15174
 
14921
15175
  // src/connect-wizard.ts
14922
- import { delimiter, dirname as dirname15, join as join34 } from "path";
14923
- import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync20, writeFileSync as writeFileSync24 } from "fs";
15176
+ import { delimiter, dirname as dirname15, join as join35 } from "path";
15177
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync25 } from "fs";
14924
15178
  import { stringify as stringify2 } from "yaml";
14925
15179
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
14926
15180
  function shouldRunWizard(opts) {
@@ -14942,7 +15196,7 @@ function defaultIsExecutable(p) {
14942
15196
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
14943
15197
  for (const dir of pathEnv.split(delimiter)) {
14944
15198
  if (!dir) continue;
14945
- const candidate = join34(dir, name);
15199
+ const candidate = join35(dir, name);
14946
15200
  if (isExecutable(candidate)) return candidate;
14947
15201
  }
14948
15202
  return void 0;
@@ -15003,9 +15257,9 @@ routes:
15003
15257
  ${routeBlock}`;
15004
15258
  }
15005
15259
  function writeNewConfig(path, text2) {
15006
- mkdirSync20(dirname15(path), { recursive: true });
15260
+ mkdirSync21(dirname15(path), { recursive: true });
15007
15261
  try {
15008
- writeFileSync24(path, text2, { mode: 384, flag: "wx" });
15262
+ writeFileSync25(path, text2, { mode: 384, flag: "wx" });
15009
15263
  return true;
15010
15264
  } catch (e) {
15011
15265
  if (e.code === "EEXIST") return false;
@@ -15039,7 +15293,7 @@ Wrote ${o.configPath}.
15039
15293
  // src/connect-json.ts
15040
15294
  import { accessSync as accessSync4, constants as constants4 } from "fs";
15041
15295
  import { homedir as homedir10 } from "os";
15042
- import { dirname as dirname16, join as join35 } from "path";
15296
+ import { dirname as dirname16, join as join36 } from "path";
15043
15297
  import { delimiter as delimiter2 } from "path";
15044
15298
  var CHOICE_EXIT_CODE = 3;
15045
15299
  function createEmitter(json2, write) {
@@ -15078,12 +15332,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
15078
15332
  const dirs = [
15079
15333
  ...pathEnv.split(delimiter2),
15080
15334
  ...(extraPath ?? "").split(delimiter2),
15081
- join35(home, ".local/bin"),
15082
- join35(home, ".claude/local"),
15335
+ join36(home, ".local/bin"),
15336
+ join36(home, ".claude/local"),
15083
15337
  "/opt/homebrew/bin",
15084
15338
  "/usr/local/bin",
15085
- join35(home, ".npm-global/bin"),
15086
- join35(home, ".volta/bin")
15339
+ join36(home, ".npm-global/bin"),
15340
+ join36(home, ".volta/bin")
15087
15341
  ];
15088
15342
  return [...new Set(dirs.filter(Boolean))];
15089
15343
  }
@@ -15119,9 +15373,9 @@ function flagWizardAnswers(f, env) {
15119
15373
  }
15120
15374
 
15121
15375
  // src/release-run.ts
15122
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync25, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync3, rmSync as rmSync16 } from "fs";
15123
- import { dirname as dirname17, join as join36 } from "path";
15124
- import { tmpdir as tmpdir4 } from "os";
15376
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync26, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, rmSync as rmSync17 } from "fs";
15377
+ import { dirname as dirname17, join as join37 } from "path";
15378
+ import { tmpdir as tmpdir5 } from "os";
15125
15379
 
15126
15380
  // src/release.ts
15127
15381
  function describeUnplaceable(unplaceable, verifiedStatus) {
@@ -15861,13 +16115,13 @@ async function bump(o, size) {
15861
16115
  }
15862
16116
  return printed;
15863
16117
  }
15864
- const files = o.repo.release.versionFiles.map((f) => join36(o.cwd, f));
16118
+ const files = o.repo.release.versionFiles.map((f) => join37(o.cwd, f));
15865
16119
  const first = files[0];
15866
16120
  if (!existsSync33(first)) {
15867
16121
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
15868
16122
  return null;
15869
16123
  }
15870
- const current = JSON.parse(readFileSync29(first, "utf8")).version;
16124
+ const current = JSON.parse(readFileSync30(first, "utf8")).version;
15871
16125
  if (!current) {
15872
16126
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
15873
16127
  return null;
@@ -15880,27 +16134,27 @@ async function bump(o, size) {
15880
16134
  return version;
15881
16135
  }
15882
16136
  for (const f of files) {
15883
- const src = readFileSync29(f, "utf8");
16137
+ const src = readFileSync30(f, "utf8");
15884
16138
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
15885
16139
  if (out === src) {
15886
16140
  o.emit.error(`no version field in ${f}`);
15887
16141
  return null;
15888
16142
  }
15889
- writeFileSync25(f, out);
16143
+ writeFileSync26(f, out);
15890
16144
  }
15891
16145
  return version;
15892
16146
  }
15893
16147
  function writeChangelog(o, version, entries) {
15894
16148
  const file = o.repo.release.changelog;
15895
16149
  if (!file || entries.length === 0) return;
15896
- const path = join36(o.cwd, file);
16150
+ const path = join37(o.cwd, file);
15897
16151
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
15898
16152
  if (o.dryRun) {
15899
16153
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
15900
16154
  return;
15901
16155
  }
15902
- const existing = existsSync33(path) ? readFileSync29(path, "utf8") : "# Changelog\n";
15903
- writeFileSync25(path, insertChangelogSection(existing, section));
16156
+ const existing = existsSync33(path) ? readFileSync30(path, "utf8") : "# Changelog\n";
16157
+ writeFileSync26(path, insertChangelogSection(existing, section));
15904
16158
  }
15905
16159
  async function confirm(o, expected) {
15906
16160
  if (!o.repo.hooks.released) return void 0;
@@ -15978,23 +16232,23 @@ async function confirmLastRelease(o, decision) {
15978
16232
  if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
15979
16233
  return { confirmed, version };
15980
16234
  }
15981
- function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
16235
+ function closeReleaseCheckout(cwd, root, dir, branch, run3 = gitOk) {
15982
16236
  try {
15983
- run2(cwd, ["worktree", "remove", "--force", dir]);
16237
+ run3(cwd, ["worktree", "remove", "--force", dir]);
15984
16238
  } finally {
15985
16239
  try {
15986
- rmSync16(root, { recursive: true, force: true });
16240
+ rmSync17(root, { recursive: true, force: true });
15987
16241
  } finally {
15988
- run2(cwd, ["worktree", "prune"]);
15989
- run2(cwd, ["branch", "-D", branch]);
16242
+ run3(cwd, ["worktree", "prune"]);
16243
+ run3(cwd, ["branch", "-D", branch]);
15990
16244
  }
15991
16245
  }
15992
16246
  }
15993
16247
  function openReleaseCheckout(o) {
15994
16248
  const { remote, base } = o.repo.branch;
15995
16249
  const branch = `crew/release-${Date.now()}`;
15996
- const root = mkdtempSync3(join36(tmpdir4(), "crew-release-"));
15997
- const dir = join36(root, "checkout");
16250
+ const root = mkdtempSync3(join37(tmpdir5(), "crew-release-"));
16251
+ const dir = join37(root, "checkout");
15998
16252
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
15999
16253
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
16000
16254
  try {
@@ -16004,10 +16258,10 @@ function openReleaseCheckout(o) {
16004
16258
  throw e;
16005
16259
  }
16006
16260
  for (const f of o.repo.worktrees.copy) {
16007
- const src = join36(o.cwd, f);
16261
+ const src = join37(o.cwd, f);
16008
16262
  if (!existsSync33(src)) continue;
16009
- mkdirSync21(dirname17(join36(dir, f)), { recursive: true });
16010
- copyFileSync(src, join36(dir, f));
16263
+ mkdirSync22(dirname17(join37(dir, f)), { recursive: true });
16264
+ copyFileSync(src, join37(dir, f));
16011
16265
  }
16012
16266
  return { dir, branch, close };
16013
16267
  }
@@ -16152,7 +16406,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
16152
16406
  let checkout;
16153
16407
  const stranded = recoverStrandedReleaseCheckouts(o.cwd);
16154
16408
  if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
16155
- const swept = sweepStaleReleaseRoots(tmpdir4());
16409
+ const swept = sweepStaleReleaseRoots(tmpdir5());
16156
16410
  if (swept.length > 0) o.emit.emit(scoped(`removed ${swept.length} stale crew-release-* temp dir(s)`), { step: "release" });
16157
16411
  try {
16158
16412
  checkout = openReleaseCheckout(o);
@@ -17325,7 +17579,7 @@ function nextRoles(decision, ran) {
17325
17579
  }
17326
17580
 
17327
17581
  // src/ship-name.ts
17328
- import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync30, renameSync as renameSync4, writeFileSync as writeFileSync26 } from "fs";
17582
+ import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
17329
17583
  import { dirname as dirname18 } from "path";
17330
17584
  import { parseDocument as parseDocument5 } from "yaml";
17331
17585
  var ShipNameError = class extends Error {
@@ -17389,7 +17643,7 @@ async function syncRouteName(i) {
17389
17643
  function readSyncedName(resolvedFile) {
17390
17644
  if (!existsSync35(resolvedFile)) return void 0;
17391
17645
  try {
17392
- const v = JSON.parse(readFileSync30(resolvedFile, "utf8")).shipName;
17646
+ const v = JSON.parse(readFileSync31(resolvedFile, "utf8")).shipName;
17393
17647
  return typeof v === "string" ? v : void 0;
17394
17648
  } catch {
17395
17649
  return void 0;
@@ -17399,22 +17653,22 @@ function writeSyncedName(resolvedFile, name) {
17399
17653
  if (!existsSync35(resolvedFile)) return;
17400
17654
  let raw;
17401
17655
  try {
17402
- raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
17656
+ raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
17403
17657
  } catch {
17404
17658
  return;
17405
17659
  }
17406
17660
  if (raw.shipName === name) return;
17407
17661
  raw.shipName = name;
17408
- mkdirSync22(dirname18(resolvedFile), { recursive: true });
17662
+ mkdirSync23(dirname18(resolvedFile), { recursive: true });
17409
17663
  const tmp = `${resolvedFile}.tmp${process.pid}`;
17410
- writeFileSync26(tmp, `${JSON.stringify(raw, null, 2)}
17664
+ writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
17411
17665
  `);
17412
- renameSync4(tmp, resolvedFile);
17666
+ renameSync5(tmp, resolvedFile);
17413
17667
  }
17414
17668
  async function reconcileShipName(i) {
17415
17669
  let local = i.ship.name;
17416
17670
  try {
17417
- local = shipNameOfYaml(readFileSync30(i.configFile, "utf8")) ?? local;
17671
+ local = shipNameOfYaml(readFileSync31(i.configFile, "utf8")) ?? local;
17418
17672
  } catch {
17419
17673
  }
17420
17674
  const resolvedFile = resolvedPathFor(i.stateDir, i.route);
@@ -17438,8 +17692,8 @@ async function reconcileShipName(i) {
17438
17692
  }
17439
17693
  if (result.status !== "ok") return result;
17440
17694
  if (result.action === "adopt" || result.action === "row-wins") {
17441
- const text2 = readFileSync30(i.configFile, "utf8");
17442
- writeFileSync26(i.configFile, setShipNameInYaml(text2, result.name));
17695
+ const text2 = readFileSync31(i.configFile, "utf8");
17696
+ writeFileSync27(i.configFile, setShipNameInYaml(text2, result.name));
17443
17697
  i.ship.name = result.name;
17444
17698
  if (result.action === "row-wins") {
17445
17699
  i.emit?.warn(
@@ -18098,7 +18352,7 @@ function describeCapability(i) {
18098
18352
  }
18099
18353
 
18100
18354
  // src/agents-sync-run.ts
18101
- import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync31, writeFileSync as writeFileSync27 } from "fs";
18355
+ import { existsSync as existsSync36, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
18102
18356
  import { dirname as dirname19 } from "path";
18103
18357
  async function runAgentsSync(target, o) {
18104
18358
  const dryRun = o.dryRun ?? false;
@@ -18132,12 +18386,12 @@ async function runAgentsSync(target, o) {
18132
18386
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
18133
18387
  if (!dryRun) {
18134
18388
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
18135
- const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
18389
+ const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync32(resolvedPath, "utf8")) : {};
18136
18390
  raw.agentPersonas = personaResult.agentPersonas;
18137
18391
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
18138
18392
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
18139
- mkdirSync23(dirname19(resolvedPath), { recursive: true });
18140
- writeFileSync27(resolvedPath, `${JSON.stringify(raw, null, 2)}
18393
+ mkdirSync24(dirname19(resolvedPath), { recursive: true });
18394
+ writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
18141
18395
  `);
18142
18396
  if (target.resolved) {
18143
18397
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -18152,7 +18406,7 @@ async function runAgentsSync(target, o) {
18152
18406
  }
18153
18407
 
18154
18408
  // src/logbook.ts
18155
- import { createHash as createHash7 } from "crypto";
18409
+ import { createHash as createHash8 } from "crypto";
18156
18410
  import { TablationClient as TablationClient4 } from "@tablation/client";
18157
18411
  var LogbookError = class extends Error {
18158
18412
  };
@@ -18225,7 +18479,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
18225
18479
  };
18226
18480
  }
18227
18481
  const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
18228
- const actualSha = createHash7("sha256").update(prompt).digest("hex");
18482
+ const actualSha = createHash8("sha256").update(prompt).digest("hex");
18229
18483
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
18230
18484
  return {
18231
18485
  entry,
@@ -18241,27 +18495,27 @@ async function showLogEntry(route2, entryId, opts = {}) {
18241
18495
 
18242
18496
  // src/repos-check.ts
18243
18497
  import { spawnSync as spawnSync9 } from "child_process";
18244
- import { readFileSync as readFileSync32 } from "fs";
18498
+ import { readFileSync as readFileSync33 } from "fs";
18245
18499
  import { basename as basename10 } from "path";
18246
18500
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
18247
18501
  function checkHookSyntax(script, shell) {
18248
18502
  const spec = resolveShell(shell);
18249
18503
  const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18250
- let run2;
18504
+ let run3;
18251
18505
  if (POSIX_SHELL.test(name)) {
18252
- run2 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
18506
+ run3 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
18253
18507
  } else if (/^(pwsh|powershell)$/.test(name)) {
18254
18508
  const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
18255
- run2 = spawnSync9(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
18509
+ run3 = spawnSync9(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
18256
18510
  encoding: "utf8",
18257
18511
  env: { ...process.env, CREW_CHECK_SCRIPT: script }
18258
18512
  });
18259
18513
  } else {
18260
18514
  return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
18261
18515
  }
18262
- if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
18263
- if (run2.status === 0) return { status: "ok" };
18264
- const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
18516
+ if (run3.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run3.error.message}` };
18517
+ if (run3.status === 0) return { status: "ok" };
18518
+ const message = `${run3.stderr ?? ""}${run3.stdout ?? ""}`.trim() || `${spec.bin} exited ${run3.status}`;
18265
18519
  return { status: "error", message };
18266
18520
  }
18267
18521
  function renderSamples(cfg, dir) {
@@ -18282,7 +18536,7 @@ function checkRepoSpec(dir, opts = {}) {
18282
18536
  }
18283
18537
  let parsed;
18284
18538
  try {
18285
- parsed = parseRepoConfig(readFileSync32(file, "utf8"), file);
18539
+ parsed = parseRepoConfig(readFileSync33(file, "utf8"), file);
18286
18540
  } catch (e) {
18287
18541
  if (!(e instanceof RepoConfigError)) throw e;
18288
18542
  return { ...empty, file, error: e.message, exitCode: 1 };
@@ -18557,7 +18811,7 @@ function existingRepoConfig(f) {
18557
18811
  }
18558
18812
 
18559
18813
  // src/repos-hooks-agent.ts
18560
- import { spawn as spawn6, execFileSync as execFileSync8 } from "child_process";
18814
+ import { spawn as spawn7, execFileSync as execFileSync8 } from "child_process";
18561
18815
  import { realpathSync as realpathSync2 } from "fs";
18562
18816
  import { isAbsolute as isAbsolute2, relative } from "path";
18563
18817
  var AgentSuggestError = class extends Error {
@@ -18675,10 +18929,10 @@ async function runAgentSuggest(o) {
18675
18929
  const before = porcelain(o.dir);
18676
18930
  const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
18677
18931
  const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
18678
- const run2 = await new Promise((resolve7) => {
18932
+ const run3 = await new Promise((resolve7) => {
18679
18933
  let out = "";
18680
18934
  let err3 = "";
18681
- const child = spawn6(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
18935
+ const child = spawn7(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
18682
18936
  let settled = false;
18683
18937
  const settle = (r) => {
18684
18938
  if (settled) return;
@@ -18710,18 +18964,18 @@ async function runAgentSuggest(o) {
18710
18964
  });
18711
18965
  child.stdin.end(buildAgentPrompt(o.sniff));
18712
18966
  });
18713
- if (run2.aborted || signal.aborted) {
18967
+ if (run3.aborted || signal.aborted) {
18714
18968
  throw new AgentSuggestError(o.signal?.aborted ? "cancelled" : "timeout", o.signal?.aborted ? "cancelled" : "the toolchain did not answer in time");
18715
18969
  }
18716
18970
  const after = porcelain(o.dir);
18717
18971
  if (before !== void 0 && after !== before) {
18718
18972
  throw new AgentSuggestError("modified_checkout", "the checkout changed during --agent; inspect it with `git status` (the agent result was discarded)");
18719
18973
  }
18720
- const parsed = parseAgentStream(run2.out, o.dir);
18721
- if (run2.spawnError || run2.code !== 0 || parsed.isError) {
18722
- const reason = classifyFailure({ code: run2.code, ...run2.spawnError ? { spawnError: run2.spawnError } : {}, output: `${parsed.text}
18723
- ${run2.err}` });
18724
- throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run2.err).trim().slice(0, 300) || `exited ${run2.code}`);
18974
+ const parsed = parseAgentStream(run3.out, o.dir);
18975
+ if (run3.spawnError || run3.code !== 0 || parsed.isError) {
18976
+ const reason = classifyFailure({ code: run3.code, ...run3.spawnError ? { spawnError: run3.spawnError } : {}, output: `${parsed.text}
18977
+ ${run3.err}` });
18978
+ throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run3.err).trim().slice(0, 300) || `exited ${run3.code}`);
18725
18979
  }
18726
18980
  const { hooks, notes } = parseAgentHooks(parsed.text);
18727
18981
  const differsFromSniff = HOOK_KEYS2.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
@@ -18729,7 +18983,7 @@ ${run2.err}` });
18729
18983
  }
18730
18984
 
18731
18985
  // src/sample-ticket.ts
18732
- import { basename as basename11, dirname as dirname20, join as join37 } from "path";
18986
+ import { basename as basename11, dirname as dirname20, join as join38 } from "path";
18733
18987
  var SAMPLE_REPO = "crew-sample";
18734
18988
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
18735
18989
  var SAMPLE_MARKER = "sample: true";
@@ -18752,8 +19006,8 @@ has something safe to work on: its test and build hooks always pass, it has no
18752
19006
  remote, and \`crew sample-ticket --remove\` deletes it.
18753
19007
  `;
18754
19008
  function sampleDir(existing, reposBase, workspace) {
18755
- if (existing.length > 0) return join37(dirname20(existing[0]), SAMPLE_REPO);
18756
- return join37(reposBase, workspace, SAMPLE_REPO);
19009
+ if (existing.length > 0) return join38(dirname20(existing[0]), SAMPLE_REPO);
19010
+ return join38(reposBase, workspace, SAMPLE_REPO);
18757
19011
  }
18758
19012
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
18759
19013
  function sampleDescription() {
@@ -18797,8 +19051,8 @@ function planSampleTicket(i) {
18797
19051
  }
18798
19052
 
18799
19053
  // src/worktree-sweep.ts
18800
- import { existsSync as existsSync37, readFileSync as readFileSync33, readdirSync as readdirSync14 } from "fs";
18801
- import { basename as basename12, dirname as dirname21, join as join38, resolve as resolve4 } from "path";
19054
+ import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync14 } from "fs";
19055
+ import { basename as basename12, dirname as dirname21, join as join39, resolve as resolve4 } from "path";
18802
19056
  function planWorktreeSweep(target, tickets, contract) {
18803
19057
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
18804
19058
  const verified = contract.statuses.verified;
@@ -18875,7 +19129,7 @@ function siblingWorktreeDirs(target) {
18875
19129
  for (const name of names) {
18876
19130
  if (!name.startsWith(stem)) continue;
18877
19131
  const m = DIR_NUMBER_SUFFIXED.exec(name);
18878
- const path = join38(parent, name);
19132
+ const path = join39(parent, name);
18879
19133
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
18880
19134
  }
18881
19135
  return out;
@@ -18978,7 +19232,7 @@ function isHandBackMerge(path, base) {
18978
19232
  if (!msgPath) return false;
18979
19233
  let first;
18980
19234
  try {
18981
- first = readFileSync33(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
19235
+ first = readFileSync34(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
18982
19236
  } catch {
18983
19237
  return false;
18984
19238
  }
@@ -19083,14 +19337,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
19083
19337
  }
19084
19338
 
19085
19339
  // src/stream-sweep.ts
19086
- import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync17, statSync as statSync10 } from "fs";
19087
- import { join as join39 } from "path";
19340
+ import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync18, statSync as statSync10 } from "fs";
19341
+ import { join as join40 } from "path";
19088
19342
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
19089
19343
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
19090
- const dir = join39(stateDir, "streams");
19344
+ const dir = join40(stateDir, "streams");
19091
19345
  if (!existsSync38(dir)) return [];
19092
19346
  const cutoff = now - retentionDays * DAY_MS2;
19093
- return readdirSync15(dir).map((name) => join39(dir, name)).filter((path) => {
19347
+ return readdirSync15(dir).map((name) => join40(dir, name)).filter((path) => {
19094
19348
  try {
19095
19349
  return statSync10(path).mtimeMs < cutoff;
19096
19350
  } catch {
@@ -19106,7 +19360,7 @@ function applyStreamSweep(paths, dryRun, log) {
19106
19360
  continue;
19107
19361
  }
19108
19362
  try {
19109
- rmSync17(path, { force: true });
19363
+ rmSync18(path, { force: true });
19110
19364
  removed++;
19111
19365
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
19112
19366
  } catch (e) {
@@ -19118,7 +19372,7 @@ function applyStreamSweep(paths, dryRun, log) {
19118
19372
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
19119
19373
  if (!existsSync38(stateDir)) return [];
19120
19374
  const cutoff = now - retentionDays * DAY_MS2;
19121
- return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join39(stateDir, name)).filter((path) => {
19375
+ return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join40(stateDir, name)).filter((path) => {
19122
19376
  try {
19123
19377
  return statSync10(path).mtimeMs < cutoff;
19124
19378
  } catch {
@@ -19128,12 +19382,12 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
19128
19382
  }
19129
19383
 
19130
19384
  // src/cli.ts
19131
- import { realpathSync as realpathSync3, readFileSync as readFileSync39, existsSync as existsSync45, mkdirSync as mkdirSync29, writeFileSync as writeFileSync32, rmSync as rmSync22, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync14, readdirSync as readdirSync20 } from "fs";
19385
+ import { realpathSync as realpathSync3, readFileSync as readFileSync40, existsSync as existsSync45, mkdirSync as mkdirSync30, writeFileSync as writeFileSync33, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync14, readdirSync as readdirSync20 } from "fs";
19132
19386
  import { createInterface } from "readline/promises";
19133
19387
 
19134
19388
  // src/periscope-cmd.ts
19135
- import { existsSync as existsSync39, mkdirSync as mkdirSync24, readFileSync as readFileSync34, rmSync as rmSync18, writeFileSync as writeFileSync28 } from "fs";
19136
- import { join as join40 } from "path";
19389
+ import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
19390
+ import { join as join41 } from "path";
19137
19391
  var PeriscopeError = class extends Error {
19138
19392
  };
19139
19393
  var LivePeriscopeError = class extends PeriscopeError {
@@ -19143,22 +19397,22 @@ var LivePeriscopeError = class extends PeriscopeError {
19143
19397
  this.data = { refused: "live_periscope_exists", ...live };
19144
19398
  }
19145
19399
  };
19146
- var priorDir = (stateDir) => join40(stateDir, "periscope-priors");
19147
- var priorPath = (stateDir, ticketRecordId) => join40(priorDir(stateDir), `${ticketRecordId}.json`);
19400
+ var priorDir = (stateDir) => join41(stateDir, "periscope-priors");
19401
+ var priorPath = (stateDir, ticketRecordId) => join41(priorDir(stateDir), `${ticketRecordId}.json`);
19148
19402
  function removePeriscopePrior(stateDir, ticketRecordId) {
19149
- rmSync18(priorPath(stateDir, ticketRecordId), { force: true });
19403
+ rmSync19(priorPath(stateDir, ticketRecordId), { force: true });
19150
19404
  }
19151
19405
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
19152
19406
  try {
19153
- mkdirSync24(priorDir(stateDir), { recursive: true });
19154
- writeFileSync28(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
19407
+ mkdirSync25(priorDir(stateDir), { recursive: true });
19408
+ writeFileSync29(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
19155
19409
  } catch {
19156
19410
  }
19157
19411
  }
19158
19412
  function readPeriscopePrior(stateDir, ticketRecordId) {
19159
19413
  try {
19160
19414
  const p = priorPath(stateDir, ticketRecordId);
19161
- return existsSync39(p) ? JSON.parse(readFileSync34(p, "utf8")) : void 0;
19415
+ return existsSync39(p) ? JSON.parse(readFileSync35(p, "utf8")) : void 0;
19162
19416
  } catch {
19163
19417
  return void 0;
19164
19418
  }
@@ -19557,7 +19811,7 @@ ${tail2(preview.output)}`);
19557
19811
  // src/periscope-idle.ts
19558
19812
  import { execFileSync as execFileSync9 } from "child_process";
19559
19813
  import { statSync as statSync11 } from "fs";
19560
- import { join as join41 } from "path";
19814
+ import { join as join42 } from "path";
19561
19815
  var MAX_SCANNED_FILES = 2e4;
19562
19816
  function newestWorktreeMtime(root) {
19563
19817
  let listing;
@@ -19577,7 +19831,7 @@ function newestWorktreeMtime(root) {
19577
19831
  if (!rel) continue;
19578
19832
  if (++scanned > MAX_SCANNED_FILES) break;
19579
19833
  try {
19580
- const m = statSync11(join41(root, rel)).mtimeMs;
19834
+ const m = statSync11(join42(root, rel)).mtimeMs;
19581
19835
  if (newest === null || m > newest) newest = m;
19582
19836
  } catch {
19583
19837
  }
@@ -19653,18 +19907,18 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
19653
19907
 
19654
19908
  // src/ssh-keys.ts
19655
19909
  import { execFileSync as execFileSync10 } from "child_process";
19656
- import { existsSync as existsSync40, mkdirSync as mkdirSync25, readFileSync as readFileSync35, chmodSync as chmodSync3 } from "fs";
19657
- import { dirname as dirname22, join as join42 } from "path";
19910
+ import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
19911
+ import { dirname as dirname22, join as join43 } from "path";
19658
19912
  var SshKeyError = class extends Error {
19659
19913
  };
19660
19914
  function shipSshKeyPathFor(stateDir) {
19661
- return join42(stateDir, "ssh", "id_ed25519");
19915
+ return join43(stateDir, "ssh", "id_ed25519");
19662
19916
  }
19663
19917
  function ensureShipSshKeypair(stateDir) {
19664
19918
  const privateKeyPath = shipSshKeyPathFor(stateDir);
19665
19919
  const publicKeyPath = `${privateKeyPath}.pub`;
19666
19920
  if (!existsSync40(privateKeyPath)) {
19667
- mkdirSync25(dirname22(privateKeyPath), { recursive: true });
19921
+ mkdirSync26(dirname22(privateKeyPath), { recursive: true });
19668
19922
  try {
19669
19923
  execFileSync10("ssh-keygen", [
19670
19924
  "-t",
@@ -19687,7 +19941,7 @@ function ensureShipSshKeypair(stateDir) {
19687
19941
  if (!existsSync40(publicKeyPath)) {
19688
19942
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
19689
19943
  }
19690
- const publicKey = readFileSync35(publicKeyPath, "utf8").trim();
19944
+ const publicKey = readFileSync36(publicKeyPath, "utf8").trim();
19691
19945
  return { privateKeyPath, publicKeyPath, publicKey };
19692
19946
  }
19693
19947
  function sshKeygenAvailable() {
@@ -19730,7 +19984,7 @@ function shipIdentityJson(s) {
19730
19984
  }
19731
19985
 
19732
19986
  // src/mcp-endpoint-registry.ts
19733
- import { createHash as createHash8 } from "crypto";
19987
+ import { createHash as createHash9 } from "crypto";
19734
19988
  function endpointsUrl(api, workspaceId, id) {
19735
19989
  const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
19736
19990
  return id ? `${base}/${id}` : base;
@@ -19800,7 +20054,7 @@ async function probeEndpointAccess(api, workspaceId) {
19800
20054
  }
19801
20055
  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";
19802
20056
  function registrationKey(url, mcpSecret, controlUrl) {
19803
- return createHash8("sha256").update(`${url}
20057
+ return createHash9("sha256").update(`${url}
19804
20058
  ${mcpSecret ?? ""}
19805
20059
  ${controlUrl ?? ""}`).digest("hex").slice(0, 16);
19806
20060
  }
@@ -19849,10 +20103,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
19849
20103
 
19850
20104
  // src/passenger-sync-daemon.ts
19851
20105
  import { createServer as createHttpServer } from "http";
19852
- import { spawn as spawn7 } from "child_process";
20106
+ import { spawn as spawn8 } from "child_process";
19853
20107
  import { randomBytes } from "crypto";
19854
- import { existsSync as existsSync41, mkdirSync as mkdirSync26, readFileSync as readFileSync36, writeFileSync as writeFileSync29, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
19855
- import { join as join43 } from "path";
20108
+ import { existsSync as existsSync41, mkdirSync as mkdirSync27, readFileSync as readFileSync37, writeFileSync as writeFileSync30, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
20109
+ import { join as join44 } from "path";
19856
20110
 
19857
20111
  // src/secret-compare.ts
19858
20112
  import { timingSafeEqual } from "crypto";
@@ -19936,26 +20190,26 @@ function runSyncDaemonFromEnv(env = process.env) {
19936
20190
  return server;
19937
20191
  }
19938
20192
  var defaultSpawn2 = (cmd, args, env) => {
19939
- const child = spawn7(cmd, args, { detached: true, stdio: "ignore", env });
20193
+ const child = spawn8(cmd, args, { detached: true, stdio: "ignore", env });
19940
20194
  child.unref();
19941
20195
  return child;
19942
20196
  };
19943
20197
  function syncDaemonStatePath(stateDir, workspaceId) {
19944
- return join43(stateDir, "passengers", `${workspaceId}-sync.json`);
20198
+ return join44(stateDir, "passengers", `${workspaceId}-sync.json`);
19945
20199
  }
19946
20200
  function readPersistedSyncDaemon(stateDir, workspaceId) {
19947
20201
  const path = syncDaemonStatePath(stateDir, workspaceId);
19948
20202
  if (!existsSync41(path)) return void 0;
19949
20203
  try {
19950
- return JSON.parse(readFileSync36(path, "utf8"));
20204
+ return JSON.parse(readFileSync37(path, "utf8"));
19951
20205
  } catch {
19952
20206
  return void 0;
19953
20207
  }
19954
20208
  }
19955
20209
  function writePersistedSyncDaemon(stateDir, state) {
19956
20210
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
19957
- mkdirSync26(join43(stateDir, "passengers"), { recursive: true });
19958
- writeFileSync29(path, `${JSON.stringify(state, null, 2)}
20211
+ mkdirSync27(join44(stateDir, "passengers"), { recursive: true });
20212
+ writeFileSync30(path, `${JSON.stringify(state, null, 2)}
19959
20213
  `);
19960
20214
  }
19961
20215
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -19975,7 +20229,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
19975
20229
  }
19976
20230
  }
19977
20231
  function readAllPersistedSyncWorkspaceIds(stateDir) {
19978
- const dir = join43(stateDir, "passengers");
20232
+ const dir = join44(stateDir, "passengers");
19979
20233
  if (!existsSync41(dir)) return [];
19980
20234
  const suffix = "-sync.json";
19981
20235
  return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -20064,8 +20318,8 @@ function describeCause(err3) {
20064
20318
  }
20065
20319
 
20066
20320
  // src/daemon.ts
20067
- import { existsSync as existsSync42, mkdirSync as mkdirSync27, readFileSync as readFileSync37, readdirSync as readdirSync17, rmSync as rmSync19, statSync as statSync12, writeFileSync as writeFileSync30 } from "fs";
20068
- import { join as join44 } from "path";
20321
+ import { existsSync as existsSync42, mkdirSync as mkdirSync28, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync12, writeFileSync as writeFileSync31 } from "fs";
20322
+ import { join as join45 } from "path";
20069
20323
  import { randomBytes as randomBytes2 } from "crypto";
20070
20324
  var DEFAULT_FLOOR_MS = 5e3;
20071
20325
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -20153,7 +20407,7 @@ function latestMtime(path) {
20153
20407
  if (!stat2.isDirectory()) return 0;
20154
20408
  let latest = stat2.mtimeMs;
20155
20409
  for (const entry of readdirSync17(path, { withFileTypes: true })) {
20156
- latest = Math.max(latest, latestMtime(join44(path, entry.name)));
20410
+ latest = Math.max(latest, latestMtime(join45(path, entry.name)));
20157
20411
  }
20158
20412
  return latest;
20159
20413
  }
@@ -20165,7 +20419,7 @@ function makeStaleChecker(paths, opts = {}) {
20165
20419
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync42(opts.reloadSentinel);
20166
20420
  }
20167
20421
  function reloadSentinelFor(stateDir) {
20168
- return join44(stateDir, "reload-requested");
20422
+ return join45(stateDir, "reload-requested");
20169
20423
  }
20170
20424
  function readReloadRequestedAt(stateDir) {
20171
20425
  try {
@@ -20175,17 +20429,17 @@ function readReloadRequestedAt(stateDir) {
20175
20429
  }
20176
20430
  }
20177
20431
  function daemonStampFor(stateDir) {
20178
- return join44(stateDir, "daemon.started");
20432
+ return join45(stateDir, "daemon.started");
20179
20433
  }
20180
20434
  function noteDaemonStarted(stateDir, now = Date.now()) {
20181
- mkdirSync27(stateDir, { recursive: true });
20182
- rmSync19(reloadSentinelFor(stateDir), { force: true });
20183
- writeFileSync30(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
20435
+ mkdirSync28(stateDir, { recursive: true });
20436
+ rmSync20(reloadSentinelFor(stateDir), { force: true });
20437
+ writeFileSync31(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
20184
20438
  `);
20185
20439
  }
20186
20440
  function readDaemonStarted(stateDir) {
20187
20441
  try {
20188
- const v = JSON.parse(readFileSync37(daemonStampFor(stateDir), "utf8")).startedAt;
20442
+ const v = JSON.parse(readFileSync38(daemonStampFor(stateDir), "utf8")).startedAt;
20189
20443
  return typeof v === "number" ? v : null;
20190
20444
  } catch {
20191
20445
  return null;
@@ -20250,7 +20504,7 @@ async function runOnePassUnguarded(o) {
20250
20504
  await hydrateApiKeys([o.route], o.getSessionStore);
20251
20505
  const emit = o.newEmitter();
20252
20506
  const decide = o.decide ?? decideCycle;
20253
- const spawn10 = o.spawnRoleAgent ?? runRoleAgent;
20507
+ const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
20254
20508
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
20255
20509
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
20256
20510
  if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
@@ -20354,7 +20608,7 @@ async function runOnePassUnguarded(o) {
20354
20608
  settle = resolve7;
20355
20609
  });
20356
20610
  const ready = (tookTicket2) => settle(tookTicket2);
20357
- const run2 = spawn10(role, decision, ctx, { ready }).then(() => {
20611
+ const run3 = spawn11(role, decision, ctx, { ready }).then(() => {
20358
20612
  breaker.recordSuccess(role);
20359
20613
  clearShipAttention(o.state, `role_parked:${role}`);
20360
20614
  clearShipAttention(o.state, workspaceAuthKey(role));
@@ -20403,8 +20657,8 @@ async function runOnePassUnguarded(o) {
20403
20657
  });
20404
20658
  if (o.inFlight) {
20405
20659
  const set = o.inFlight;
20406
- set.add(run2);
20407
- void run2.finally(() => set.delete(run2));
20660
+ set.add(run3);
20661
+ void run3.finally(() => set.delete(run3));
20408
20662
  }
20409
20663
  const reported = TICKET_ROLES.has(role) ? await settled : true;
20410
20664
  const tookTicket = reported !== false;
@@ -20740,7 +20994,7 @@ async function buildEnvironment(route2, ship, ticket) {
20740
20994
  }
20741
20995
 
20742
20996
  // src/control-api.ts
20743
- import { createHash as createHash9 } from "crypto";
20997
+ import { createHash as createHash10 } from "crypto";
20744
20998
  import {
20745
20999
  chmodSync as chmodSync4,
20746
21000
  closeSync as closeSync5,
@@ -20749,17 +21003,17 @@ import {
20749
21003
  openSync as openSync5,
20750
21004
  readSync as readSync3,
20751
21005
  readdirSync as readdirSync18,
20752
- rmSync as rmSync20,
21006
+ rmSync as rmSync21,
20753
21007
  statSync as statSync13,
20754
21008
  watch
20755
21009
  } from "fs";
20756
21010
  import { createConnection } from "net";
20757
21011
  import { createServer } from "http";
20758
- import { basename as basename13, dirname as dirname23, join as join45 } from "path";
20759
- import { execFile as execFile2, spawn as spawn9 } from "child_process";
21012
+ import { basename as basename13, dirname as dirname23, join as join46 } from "path";
21013
+ import { execFile as execFile2, spawn as spawn10 } from "child_process";
20760
21014
 
20761
21015
  // src/periscope-control.ts
20762
- import { spawn as spawn8 } from "child_process";
21016
+ import { spawn as spawn9 } from "child_process";
20763
21017
  var reply = (status2, body) => ({ status: status2, body });
20764
21018
  var err = (status2, message) => reply(status2, { error: message });
20765
21019
  var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
@@ -20950,7 +21204,7 @@ async function handlePeriscopes(h, q) {
20950
21204
  }
20951
21205
  function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20952
21206
  return new Promise((resolve7) => {
20953
- const child = spawn8(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
21207
+ const child = spawn9(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
20954
21208
  let stdout = "";
20955
21209
  let stderr = "";
20956
21210
  let pending = "";
@@ -20984,7 +21238,7 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20984
21238
  // src/control-api.ts
20985
21239
  var PAUSABLE = ["dev", "design", "qa"];
20986
21240
  var CONTROL_SOCKET_NAME = "control.sock";
20987
- var controlSocketFor = (stateDir) => join45(stateDir, CONTROL_SOCKET_NAME);
21241
+ var controlSocketFor = (stateDir) => join46(stateDir, CONTROL_SOCKET_NAME);
20988
21242
  var MAX_SOCKET_PATH = 100;
20989
21243
  var MAX_TAIL_BYTES = 1024 * 1024;
20990
21244
  var MAX_WAIT_SECONDS = 30;
@@ -21000,7 +21254,7 @@ var ControlAuth = class {
21000
21254
  if (!token) return "unauthorized";
21001
21255
  for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
21002
21256
  const now = (this.o.now ?? Date.now)();
21003
- const id = createHash9("sha256").update(token).digest("hex");
21257
+ const id = createHash10("sha256").update(token).digest("hex");
21004
21258
  const hit = this.cache.get(id);
21005
21259
  if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
21006
21260
  const verdict = await this.ask(token);
@@ -21021,7 +21275,7 @@ var ControlAuth = class {
21021
21275
  for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
21022
21276
  for (const k of this.o.workspaceSecrets?.(workspaceId) ?? []) if (k && secretsMatch(token, k)) return "ok";
21023
21277
  const now = (this.o.now ?? Date.now)();
21024
- const id = `${workspaceId}:${createHash9("sha256").update(token).digest("hex")}`;
21278
+ const id = `${workspaceId}:${createHash10("sha256").update(token).digest("hex")}`;
21025
21279
  const hit = this.cache.get(id);
21026
21280
  if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
21027
21281
  const verdict = await this.ask(token, [workspaceId]);
@@ -21071,7 +21325,7 @@ function candidates(live, rotated) {
21071
21325
  const out = [live];
21072
21326
  if (!rotated) return out;
21073
21327
  try {
21074
- for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join45(dirname23(live), n));
21328
+ for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname23(live), n));
21075
21329
  } catch {
21076
21330
  }
21077
21331
  return out;
@@ -21269,9 +21523,9 @@ async function route(deps, method, url, req, abort) {
21269
21523
  if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
21270
21524
  if (p === "/streams" && get2) return reply2(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
21271
21525
  if (p.startsWith("/streams/") && get2) {
21272
- const run2 = decodeURIComponent(p.slice("/streams/".length));
21273
- if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
21274
- const file = join45(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
21526
+ const run3 = decodeURIComponent(p.slice("/streams/".length));
21527
+ if (!RUN_NAME_RE.test(run3) || run3.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run3)) return err2(400, "not a run name");
21528
+ const file = join46(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
21275
21529
  if (!existsSync43(file)) return err2(404, "no such run");
21276
21530
  return tailReply(file, url.searchParams, false, abort);
21277
21531
  }
@@ -21334,7 +21588,7 @@ async function startControlServer(deps) {
21334
21588
  log(`control API not started: socket path is too long for a unix socket (${path})`);
21335
21589
  return null;
21336
21590
  }
21337
- if (await socketIsStale(path)) rmSync20(path, { force: true });
21591
+ if (await socketIsStale(path)) rmSync21(path, { force: true });
21338
21592
  else if (existsSync43(path)) {
21339
21593
  log(`control API not started: ${path} is in use by another process`);
21340
21594
  return null;
@@ -21381,7 +21635,7 @@ async function startControlServer(deps) {
21381
21635
  path,
21382
21636
  close: () => new Promise((done) => {
21383
21637
  server.close(() => {
21384
- rmSync20(path, { force: true });
21638
+ rmSync21(path, { force: true });
21385
21639
  done();
21386
21640
  });
21387
21641
  })
@@ -21390,19 +21644,19 @@ async function startControlServer(deps) {
21390
21644
  });
21391
21645
  }
21392
21646
  var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
21393
- function runDetached(commands, spawnFn = spawn9) {
21647
+ function runDetached(commands, spawnFn = spawn10) {
21394
21648
  const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
21395
21649
  const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
21396
21650
  child.on("error", () => {
21397
21651
  });
21398
21652
  child.unref();
21399
21653
  }
21400
- function schedulerHandler(control, run2 = runDetached) {
21654
+ function schedulerHandler(control, run3 = runDetached) {
21401
21655
  return (action) => {
21402
21656
  const { startCommand: start, stopCommand: stop } = control;
21403
21657
  if (!start || !stop) return { accepted: false, detail: "this host has no service manager for the daemon (cron or none)" };
21404
21658
  const commands = action === "start" ? [start] : action === "stop" ? [stop] : [stop, start];
21405
- run2(commands);
21659
+ run3(commands);
21406
21660
  return { accepted: true, detail: `${action} requested` };
21407
21661
  };
21408
21662
  }
@@ -21421,53 +21675,53 @@ function crewJson(args, timeoutMs = 6e4) {
21421
21675
 
21422
21676
  // src/periscope-mcp.ts
21423
21677
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
21424
- import { basename as basename14, dirname as dirname24, isAbsolute as isAbsolute3, join as join47, relative as relative2, resolve as resolve5, sep } from "path";
21678
+ import { basename as basename14, dirname as dirname24, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
21425
21679
  import { z } from "zod";
21426
21680
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21427
21681
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21428
21682
 
21429
21683
  // src/periscope-endpoints.ts
21430
21684
  import { randomBytes as randomBytes3 } from "crypto";
21431
- import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync28, readdirSync as readdirSync19, readFileSync as readFileSync38, rmSync as rmSync21, writeFileSync as writeFileSync31 } from "fs";
21432
- import { join as join46 } from "path";
21433
- var secretDir = (stateDir) => join46(stateDir, "periscope-mcp");
21434
- var secretFile = (stateDir, id) => join46(secretDir(stateDir), `${id}.json`);
21435
- var endpointDir = (stateDir) => join46(stateDir, "periscope-endpoints");
21436
- var endpointFile = (stateDir, id) => join46(endpointDir(stateDir), `${id}.json`);
21685
+ import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync29, readdirSync as readdirSync19, readFileSync as readFileSync39, rmSync as rmSync22, writeFileSync as writeFileSync32 } from "fs";
21686
+ import { join as join47 } from "path";
21687
+ var secretDir = (stateDir) => join47(stateDir, "periscope-mcp");
21688
+ var secretFile = (stateDir, id) => join47(secretDir(stateDir), `${id}.json`);
21689
+ var endpointDir = (stateDir) => join47(stateDir, "periscope-endpoints");
21690
+ var endpointFile = (stateDir, id) => join47(endpointDir(stateDir), `${id}.json`);
21437
21691
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
21438
- mkdirSync28(secretDir(stateDir), { recursive: true, mode: 448 });
21692
+ mkdirSync29(secretDir(stateDir), { recursive: true, mode: 448 });
21439
21693
  const secret = randomBytes3(32).toString("base64url");
21440
21694
  const file = secretFile(stateDir, ticketRecordId);
21441
- writeFileSync31(file, `${JSON.stringify({ secret })}
21695
+ writeFileSync32(file, `${JSON.stringify({ secret })}
21442
21696
  `, { mode: 384 });
21443
21697
  chmodSync5(file, 384);
21444
21698
  return secret;
21445
21699
  }
21446
21700
  function readPeriscopeSecret(stateDir, ticketRecordId) {
21447
21701
  try {
21448
- const v = JSON.parse(readFileSync38(secretFile(stateDir, ticketRecordId), "utf8"));
21702
+ const v = JSON.parse(readFileSync39(secretFile(stateDir, ticketRecordId), "utf8"));
21449
21703
  return typeof v.secret === "string" && v.secret ? v.secret : void 0;
21450
21704
  } catch {
21451
21705
  return void 0;
21452
21706
  }
21453
21707
  }
21454
21708
  function removePeriscopeSecret(stateDir, ticketRecordId) {
21455
- rmSync21(secretFile(stateDir, ticketRecordId), { force: true });
21709
+ rmSync22(secretFile(stateDir, ticketRecordId), { force: true });
21456
21710
  }
21457
21711
  function readPeriscopeEndpoint(stateDir, ticketRecordId) {
21458
21712
  try {
21459
- return JSON.parse(readFileSync38(endpointFile(stateDir, ticketRecordId), "utf8"));
21713
+ return JSON.parse(readFileSync39(endpointFile(stateDir, ticketRecordId), "utf8"));
21460
21714
  } catch {
21461
21715
  return void 0;
21462
21716
  }
21463
21717
  }
21464
21718
  function writePeriscopeEndpoint(stateDir, rec) {
21465
- mkdirSync28(endpointDir(stateDir), { recursive: true });
21466
- writeFileSync31(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
21719
+ mkdirSync29(endpointDir(stateDir), { recursive: true });
21720
+ writeFileSync32(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
21467
21721
  `);
21468
21722
  }
21469
21723
  function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
21470
- rmSync21(endpointFile(stateDir, ticketRecordId), { force: true });
21724
+ rmSync22(endpointFile(stateDir, ticketRecordId), { force: true });
21471
21725
  }
21472
21726
  function listPeriscopeEndpoints(stateDir) {
21473
21727
  const dir = endpointDir(stateDir);
@@ -21574,13 +21828,13 @@ async function resolveInWorktree(root, requested) {
21574
21828
  let abs;
21575
21829
  if (isAbsolute3(requested)) {
21576
21830
  abs = resolve5(requested);
21577
- if (isInside(root, abs)) abs = join47(realRoot, relative2(root, abs));
21831
+ if (isInside(root, abs)) abs = join48(realRoot, relative2(root, abs));
21578
21832
  } else {
21579
21833
  abs = resolve5(realRoot, requested);
21580
21834
  }
21581
21835
  if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21582
21836
  const { real, rest } = await realAncestor(abs);
21583
- const final = rest ? join47(real, rest) : real;
21837
+ const final = rest ? join48(real, rest) : real;
21584
21838
  if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21585
21839
  if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
21586
21840
  return final;
@@ -21648,7 +21902,7 @@ function createPeriscopeMcpServer(d, sc) {
21648
21902
  const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
21649
21903
  const lines2 = await Promise.all(entries.map(async (e) => {
21650
21904
  if (e.isDirectory()) return `[DIR] ${e.name}`;
21651
- const size = (await lstat(join47(abs, e.name))).size;
21905
+ const size = (await lstat(join48(abs, e.name))).size;
21652
21906
  return `[FILE] ${e.name} (${size} bytes)`;
21653
21907
  }));
21654
21908
  return text(lines2.join("\n") || "(empty)");
@@ -21714,7 +21968,7 @@ function createPeriscopeMcpServer(d, sc) {
21714
21968
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
21715
21969
  }, async ({ path }) => {
21716
21970
  const parent = await inTree(dirname24(path) === "" ? "." : dirname24(path));
21717
- const abs = join47(parent, basename14(path));
21971
+ const abs = join48(parent, basename14(path));
21718
21972
  if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
21719
21973
  const st = await lstat(abs).catch(() => void 0);
21720
21974
  if (!st) return text(`${path} does not exist`, true);
@@ -22121,6 +22375,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
22121
22375
  "rotate-passenger-url",
22122
22376
  "run",
22123
22377
  "sample-ticket",
22378
+ "sandbox",
22124
22379
  "selftest",
22125
22380
  "shell-init",
22126
22381
  "ship",
@@ -22200,7 +22455,7 @@ if (command === "provision") {
22200
22455
  process.exit(outcome.code);
22201
22456
  }
22202
22457
  if (command === "pool-member") {
22203
- const pool = poolConfigFrom(argv, process.env, () => readFileSync39(0, "utf8"));
22458
+ const pool = poolConfigFrom(argv, process.env, () => readFileSync40(0, "utf8"));
22204
22459
  if (typeof pool === "string") {
22205
22460
  process.stderr.write(`crew pool-member: ${pool}
22206
22461
  `);
@@ -22241,9 +22496,9 @@ if (command === "passenger-sync-daemon") {
22241
22496
  return m ? m[1] : null;
22242
22497
  }, routeRelayUrl = function(route3) {
22243
22498
  const ws = route3.resolved?.workspaceId;
22244
- const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
22245
- if (!cfg.ship.relayHost || !slug) return null;
22246
- return relayBaseUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
22499
+ const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
22500
+ if (!cfg.ship.relayHost || !slug2) return null;
22501
+ return relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
22247
22502
  }, passengerApiKeys = function(routes) {
22248
22503
  const keys = /* @__PURE__ */ new Map();
22249
22504
  for (const r of routes) {
@@ -22308,8 +22563,8 @@ if (command === "passenger-sync-daemon") {
22308
22563
  stateDir: cfg.ship.stateDir,
22309
22564
  shipName: cfg.ship.name,
22310
22565
  apiFor: endpointApiFor(routes),
22311
- urlFor: (slug) => publicUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
22312
- controlUrlFor: (slug) => `${relayBaseUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control`,
22566
+ urlFor: (slug2) => publicUrlFor(slug2, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
22567
+ controlUrlFor: (slug2) => `${relayBaseUrlFor(slug2, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control`,
22313
22568
  warn: (m) => emit.warn(m, { step: "passengers" })
22314
22569
  };
22315
22570
  }, releaseRefused = function() {
@@ -22329,8 +22584,20 @@ if (command === "passenger-sync-daemon") {
22329
22584
  const out = refreshBaseBranch(r.dir, remote, base, true);
22330
22585
  const unsafe = baseBranchUnsafe(out);
22331
22586
  return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
22587
+ }, sandboxTickDeps = function(emit2, c) {
22588
+ return {
22589
+ stateDir: cfg.ship.stateDir,
22590
+ run: spawnBuildRunner(),
22591
+ hasImage: engineHasImage(),
22592
+ removeImage: engineRemoveImage(),
22593
+ raise: (item) => {
22594
+ void raiseShipAttention({ state, emit: emit2, route: c, ship: cfg.ship }, item);
22595
+ },
22596
+ clear: (key) => clearShipAttention(state, key),
22597
+ log: (m) => emit2.emit(m)
22598
+ };
22332
22599
  };
22333
- requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
22600
+ requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth, sandboxTickDeps2 = sandboxTickDeps;
22334
22601
  let cfg;
22335
22602
  let firstRun;
22336
22603
  let firstRunFromFlags = false;
@@ -22401,7 +22668,7 @@ if (command === "passenger-sync-daemon") {
22401
22668
  throw e;
22402
22669
  }
22403
22670
  if (command === "credential") {
22404
- const r = runCredential(positional[1], readFileSync39(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
22671
+ const r = runCredential(positional[1], readFileSync40(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
22405
22672
  process.stdout.write(r.out);
22406
22673
  process.stderr.write(r.err);
22407
22674
  process.exit(r.code);
@@ -22413,9 +22680,9 @@ if (command === "passenger-sync-daemon") {
22413
22680
  forges: cfg.ship.forges,
22414
22681
  stateDir: cfg.ship.stateDir,
22415
22682
  configFile: cfg.configFile,
22416
- readFile: (p) => readFileSync39(p, "utf8"),
22417
- writeFile: (p, t) => writeFileSync32(p, t),
22418
- stdin: () => readFileSync39(0, "utf8"),
22683
+ readFile: (p) => readFileSync40(p, "utf8"),
22684
+ writeFile: (p, t) => writeFileSync33(p, t),
22685
+ stdin: () => readFileSync40(0, "utf8"),
22419
22686
  env: process.env
22420
22687
  }
22421
22688
  );
@@ -22435,7 +22702,7 @@ if (command === "passenger-sync-daemon") {
22435
22702
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
22436
22703
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
22437
22704
  const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
22438
- const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
22705
+ const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "sandbox" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
22439
22706
  let route2;
22440
22707
  try {
22441
22708
  route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -22698,7 +22965,7 @@ if (command === "passenger-sync-daemon") {
22698
22965
  }
22699
22966
  if (host === "linux" && hasSystemd) {
22700
22967
  const label = labelFor(CREW_HOME, "run");
22701
- const staleTimer = join48(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22968
+ const staleTimer = join49(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22702
22969
  if (existsSync45(staleTimer)) {
22703
22970
  process.stdout.write(
22704
22971
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -22761,7 +23028,7 @@ if (command === "passenger-sync-daemon") {
22761
23028
  for (const a of plan.actions) {
22762
23029
  if (a.kind === "enable") {
22763
23030
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
22764
- writeFileSync32(cfg.configFile, planEnable(readFileSync39(cfg.configFile, "utf8"), a.route));
23031
+ writeFileSync33(cfg.configFile, planEnable(readFileSync40(cfg.configFile, "utf8"), a.route));
22765
23032
  enabledSound.add(a.route);
22766
23033
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
22767
23034
  } else {
@@ -22780,6 +23047,14 @@ ${changed2.map((c) => ` - ${c}
22780
23047
  async function resolvedRepos(c) {
22781
23048
  return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
22782
23049
  }
23050
+ async function sandboxReposOf(routes) {
23051
+ const out = /* @__PURE__ */ new Map();
23052
+ for (const c of routes) {
23053
+ const { settled } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
23054
+ for (const r of settled) if (r.config.sandbox && !out.has(r.name)) out.set(r.name, { name: r.name, decl: r.config.sandbox });
23055
+ }
23056
+ return [...out.values()];
23057
+ }
22783
23058
  async function resolveOneRepo(c, t) {
22784
23059
  {
22785
23060
  const id = repoIdForName(c, t.name);
@@ -23884,7 +24159,7 @@ ${cu.tail.trimEnd()}
23884
24159
  process.exit(1);
23885
24160
  }
23886
24161
  requireArmed("run the daemon loop");
23887
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join48(CREW_HOME, "src"), join48(CREW_HOME, "dist"), join48(CREW_HOME, "bin")];
24162
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join49(CREW_HOME, "src"), join49(CREW_HOME, "dist"), join49(CREW_HOME, "bin")];
23888
24163
  const configPaths = daemonConfigPaths(cfg);
23889
24164
  watchPaths.push(...configPaths);
23890
24165
  noteDaemonStarted(cfg.ship.stateDir);
@@ -23950,6 +24225,7 @@ ${cu.tail.trimEnd()}
23950
24225
  });
23951
24226
  if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
23952
24227
  const inFlight = /* @__PURE__ */ new Set();
24228
+ let sandboxBuilding = false;
23953
24229
  const stopReason = await runDaemonLoop({
23954
24230
  inFlight,
23955
24231
  onStaleDeferred: ({ runs, waitedMs }) => emit.warn(
@@ -23995,6 +24271,12 @@ ${cu.tail.trimEnd()}
23995
24271
  ),
23996
24272
  checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
23997
24273
  onPass: (result) => {
24274
+ if (!sandboxBuilding) {
24275
+ sandboxBuilding = true;
24276
+ void sandboxReposOf(cfg.routes.filter((r) => r.enabled)).then((repos) => repos.length ? sandboxBuildTick(repos, sandboxTickDeps(emit, route2)) : null).catch((err3) => emit.warn(`sandbox image build tick failed: ${err3 instanceof Error ? err3.message : String(err3)}`)).finally(() => {
24277
+ sandboxBuilding = false;
24278
+ });
24279
+ }
23998
24280
  if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
23999
24281
  for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
24000
24282
  }
@@ -24097,9 +24379,9 @@ ${cu.tail.trimEnd()}
24097
24379
  `);
24098
24380
  process.exit(2);
24099
24381
  }
24100
- let run2;
24382
+ let run3;
24101
24383
  try {
24102
- run2 = await runAgentsSync(target, {
24384
+ run3 = await runAgentsSync(target, {
24103
24385
  userAgent: cfg.ship.userAgent,
24104
24386
  stateDir: cfg.ship.stateDir,
24105
24387
  dryRun,
@@ -24117,7 +24399,7 @@ ${cu.tail.trimEnd()}
24117
24399
  }
24118
24400
  throw e;
24119
24401
  }
24120
- const { personaResult, skillResult } = run2;
24402
+ const { personaResult, skillResult } = run3;
24121
24403
  if (flag("revoke-seat-keys")) {
24122
24404
  for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
24123
24405
  process.stdout.write(`${describeRevokeOutcome(o)}
@@ -24138,7 +24420,7 @@ ${cu.tail.trimEnd()}
24138
24420
  }
24139
24421
  }
24140
24422
  }
24141
- if (run2.diverged) process.exitCode = 1;
24423
+ if (run3.diverged) process.exitCode = 1;
24142
24424
  break;
24143
24425
  }
24144
24426
  case "skills": {
@@ -24180,10 +24462,10 @@ ${cu.tail.trimEnd()}
24180
24462
  `);
24181
24463
  if (!dryRun) {
24182
24464
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
24183
- const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync39(resolvedPath, "utf8")) : {};
24465
+ const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync40(resolvedPath, "utf8")) : {};
24184
24466
  raw.agentSkills = result.agentSkills;
24185
- mkdirSync29(dirOf2(resolvedPath), { recursive: true });
24186
- writeFileSync32(resolvedPath, `${JSON.stringify(raw, null, 2)}
24467
+ mkdirSync30(dirOf2(resolvedPath), { recursive: true });
24468
+ writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
24187
24469
  `);
24188
24470
  }
24189
24471
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -24206,7 +24488,7 @@ ${cu.tail.trimEnd()}
24206
24488
  throw e;
24207
24489
  }
24208
24490
  const oldName = cfg.ship.name;
24209
- writeFileSync32(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), newName));
24491
+ writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), newName));
24210
24492
  cfg.ship.name = newName;
24211
24493
  let renameMachineId;
24212
24494
  try {
@@ -24253,7 +24535,7 @@ ${cu.tail.trimEnd()}
24253
24535
  process.exit(2);
24254
24536
  };
24255
24537
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
24256
- const configText = readFileSync39(cfg.configFile, "utf8");
24538
+ const configText = readFileSync40(cfg.configFile, "utf8");
24257
24539
  if (sub === "check") {
24258
24540
  try {
24259
24541
  const routeArg = value("route") ?? positional[2];
@@ -24293,7 +24575,7 @@ ${cu.tail.trimEnd()}
24293
24575
  }
24294
24576
  }, read: (r) => {
24295
24577
  try {
24296
- return readFileSync39(resolvePath(dir, r), "utf8");
24578
+ return readFileSync40(resolvePath(dir, r), "utf8");
24297
24579
  } catch {
24298
24580
  return void 0;
24299
24581
  }
@@ -24335,19 +24617,19 @@ ${cu.tail.trimEnd()}
24335
24617
  let cleared;
24336
24618
  if (clear2) {
24337
24619
  const plan = clearRouteHooks(configText, a.route, a.name);
24338
- if (plan.removed.length > 0) writeFileSync32(cfg.configFile, plan.text);
24620
+ if (plan.removed.length > 0) writeFileSync33(cfg.configFile, plan.text);
24339
24621
  cleared = { file: cfg.configFile, removed: plan.removed };
24340
24622
  }
24341
24623
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
24342
24624
  if (write === "route") {
24343
24625
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
24344
- writeFileSync32(cfg.configFile, plan.text);
24626
+ writeFileSync33(cfg.configFile, plan.text);
24345
24627
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
24346
24628
  } else if (write === "repo") {
24347
24629
  const have = existingRepoConfig(files);
24348
24630
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
24349
24631
  const file = resolvePath(dir, ".crew.yaml");
24350
- writeFileSync32(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24632
+ writeFileSync33(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24351
24633
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
24352
24634
  }
24353
24635
  if (flag("json")) {
@@ -24444,7 +24726,7 @@ ${cu.tail.trimEnd()}
24444
24726
  let resolvedRemotes;
24445
24727
  if (existsSync45(resolvedFile)) {
24446
24728
  try {
24447
- resolvedRemotes = JSON.parse(readFileSync39(resolvedFile, "utf8"));
24729
+ resolvedRemotes = JSON.parse(readFileSync40(resolvedFile, "utf8"));
24448
24730
  } catch {
24449
24731
  }
24450
24732
  }
@@ -24465,7 +24747,7 @@ ${cu.tail.trimEnd()}
24465
24747
  isCheckout,
24466
24748
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
24467
24749
  try {
24468
- return Object.keys(parseRepoConfig(readFileSync39(resolvePath(d, f), "utf8"), f).hooks).length > 0;
24750
+ return Object.keys(parseRepoConfig(readFileSync40(resolvePath(d, f), "utf8"), f).hooks).length > 0;
24469
24751
  } catch {
24470
24752
  return false;
24471
24753
  }
@@ -24514,7 +24796,7 @@ ${cu.tail.trimEnd()}
24514
24796
  throw new ReposError(`${e.message}${authHint}
24515
24797
  crew.yaml was not changed`);
24516
24798
  }
24517
- writeFileSync32(cfg.configFile, plan2.text);
24799
+ writeFileSync33(cfg.configFile, plan2.text);
24518
24800
  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:
24519
24801
  `);
24520
24802
  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}`);
@@ -24574,14 +24856,14 @@ ${cu.tail.trimEnd()}
24574
24856
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
24575
24857
  `);
24576
24858
  if (existsSync45(resolvedFile)) {
24577
- const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
24859
+ const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
24578
24860
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
24579
24861
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
24580
- writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
24862
+ writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
24581
24863
  `);
24582
24864
  }
24583
24865
  }
24584
- writeFileSync32(cfg.configFile, plan.text);
24866
+ writeFileSync33(cfg.configFile, plan.text);
24585
24867
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
24586
24868
  `);
24587
24869
  } catch (e) {
@@ -24950,7 +25232,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24950
25232
  process.exit(2);
24951
25233
  }
24952
25234
  if (found.shipNameAdopted && !dryRun) {
24953
- writeFileSync32(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), found.shipNameAdopted));
25235
+ writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), found.shipNameAdopted));
24954
25236
  cfg.ship.name = found.shipNameAdopted;
24955
25237
  }
24956
25238
  if (found.shipRow) {
@@ -24996,7 +25278,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24996
25278
  const hadPreviousFile = existsSync45(resolvedPath);
24997
25279
  if (hadPreviousFile) {
24998
25280
  try {
24999
- previous = JSON.parse(readFileSync39(resolvedPath, "utf8"));
25281
+ previous = JSON.parse(readFileSync40(resolvedPath, "utf8"));
25000
25282
  } catch {
25001
25283
  }
25002
25284
  }
@@ -25126,12 +25408,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25126
25408
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
25127
25409
  `);
25128
25410
  } else {
25129
- mkdirSync29(dirname25(resolvedPath), { recursive: true });
25411
+ mkdirSync30(dirname25(resolvedPath), { recursive: true });
25130
25412
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
25131
25413
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
25132
- writeFileSync32(tmpPath, `${JSON.stringify(resolved, null, 2)}
25414
+ writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
25133
25415
  `);
25134
- renameSync5(tmpPath, resolvedPath);
25416
+ renameSync6(tmpPath, resolvedPath);
25135
25417
  process.stderr.write(`Resolved ids written to ${resolvedPath}
25136
25418
  `);
25137
25419
  }
@@ -25175,8 +25457,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25175
25457
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
25176
25458
  `);
25177
25459
  } else {
25178
- mkdirSync29(dirname25(apiKeyFile), { recursive: true });
25179
- writeFileSync32(apiKeyFile, `${apiKeyVar}=${apiKey}
25460
+ mkdirSync30(dirname25(apiKeyFile), { recursive: true });
25461
+ writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
25180
25462
  `, { mode: 384 });
25181
25463
  process.stderr.write(`API key written to ${apiKeyFile}
25182
25464
  `);
@@ -25309,7 +25591,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25309
25591
  if (syncRoute?.resolved) {
25310
25592
  try {
25311
25593
  process.stderr.write("\n");
25312
- const run2 = await runAgentsSync(syncRoute, {
25594
+ const run3 = await runAgentsSync(syncRoute, {
25313
25595
  userAgent: cfg.ship.userAgent,
25314
25596
  stateDir: cfg.ship.stateDir,
25315
25597
  say: (l) => process.stderr.write(` ${l}
@@ -25324,19 +25606,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25324
25606
  if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role, p.tables)}
25325
25607
  `);
25326
25608
  }
25327
- if (run2.readOnly) {
25328
- const missing = run2.personaResult.outcomes.some((p) => p.action === "missing");
25609
+ if (run3.readOnly) {
25610
+ const missing = run3.personaResult.outcomes.some((p) => p.action === "missing");
25329
25611
  process.stderr.write(missing ? " A workspace admin has to open the Crew app or run `crew agents sync` once before this ship's seats can run.\n" : " Seats are linked to the workspace's existing personas.\n");
25330
25612
  }
25331
- connectEvents.emit("agents_synced", run2.readOnly ? "agents linked (read-only key)" : "agents synced", {
25613
+ connectEvents.emit("agents_synced", run3.readOnly ? "agents linked (read-only key)" : "agents synced", {
25332
25614
  route: route3,
25333
- admin: !run2.readOnly,
25334
- personas: run2.personaResult.outcomes,
25335
- skills: run2.skillResult?.outcomes ?? [],
25336
- diverged: run2.diverged
25615
+ admin: !run3.readOnly,
25616
+ personas: run3.personaResult.outcomes,
25617
+ skills: run3.skillResult?.outcomes ?? [],
25618
+ diverged: run3.diverged
25337
25619
  });
25338
25620
  for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route3, ...m });
25339
- if (run2.diverged) process.exitCode = 1;
25621
+ if (run3.diverged) process.exitCode = 1;
25340
25622
  } catch (e) {
25341
25623
  if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
25342
25624
  else throw e;
@@ -25724,6 +26006,70 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25724
26006
  if (!planIsUsable(planWt)) process.exitCode = 1;
25725
26007
  break;
25726
26008
  }
26009
+ case "sandbox": {
26010
+ const subS = positional[1];
26011
+ if (subS !== "build" && subS !== "images") {
26012
+ process.stderr.write("crew sandbox build [REPO] [--route ROUTE] [--json]\ncrew sandbox images [--route ROUTE] [--json]\n");
26013
+ process.exit(2);
26014
+ }
26015
+ const jsonS = flag("json");
26016
+ let routesS;
26017
+ try {
26018
+ routesS = value("route") ? [findRoute(cfg, value("route"))] : cfg.routes.filter((r) => r.enabled);
26019
+ } catch (e) {
26020
+ if (e instanceof ConfigError) {
26021
+ process.stderr.write(`crew: ${e.message}
26022
+ `);
26023
+ process.exit(2);
26024
+ }
26025
+ throw e;
26026
+ }
26027
+ const declared = await sandboxReposOf(routesS);
26028
+ if (subS === "images") {
26029
+ const stateS = readSandboxState(cfg.ship.stateDir);
26030
+ const rows = declared.map((r) => {
26031
+ const plan = planSandbox(r.name, r.decl);
26032
+ return { repo: r.name, wants: plan.tag, tags: stateS[r.name]?.tags ?? [], ...sandboxImageStatus(stateS, plan) };
26033
+ });
26034
+ if (jsonS) {
26035
+ process.stdout.write(`${JSON.stringify(rows, null, 2)}
26036
+ `);
26037
+ break;
26038
+ }
26039
+ if (rows.length === 0) process.stdout.write("no repository declares a sandbox: section in its .crew.yaml\n");
26040
+ for (const r of rows) {
26041
+ process.stdout.write(`${r.repo}: ${r.status} \u2014 ${r.detail}
26042
+ `);
26043
+ for (const t of r.tags) {
26044
+ const ageH = Math.max(0, Math.round((Date.now() - Date.parse(t.builtAt)) / 36e5));
26045
+ process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago)
26046
+ `);
26047
+ }
26048
+ }
26049
+ break;
26050
+ }
26051
+ const only = positional[2];
26052
+ const targets = only ? declared.filter((r) => r.name === only) : declared;
26053
+ if (targets.length === 0) {
26054
+ process.stderr.write(only ? `crew: no repository "${only}" declares a sandbox: section
26055
+ ` : "crew: no repository declares a sandbox: section in its .crew.yaml\n");
26056
+ process.exit(2);
26057
+ }
26058
+ const emitS = new Emitter({ route: routesS[0].route, eventFile: eventFileFor(cfg.ship.stateDir), rotateBytes: cfg.ship.eventsRotateBytes, logFile: cfg.ship.logFile, console: (l) => process.stderr.write(`${l}
26059
+ `) });
26060
+ const depsS = sandboxTickDeps(emitS, routesS[0]);
26061
+ let failed = false;
26062
+ for (const t of targets) {
26063
+ await sandboxBuildTick([t], depsS, true);
26064
+ const after = readSandboxState(cfg.ship.stateDir);
26065
+ const st = sandboxImageStatus(after, planSandbox(t.name, t.decl));
26066
+ if (st.status === "failed") failed = true;
26067
+ process.stdout.write(`${t.name}: ${st.status} \u2014 ${st.detail}
26068
+ `);
26069
+ }
26070
+ if (failed) process.exit(1);
26071
+ break;
26072
+ }
25727
26073
  case "periscope": {
25728
26074
  const subP = positional[1];
25729
26075
  if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
@@ -26006,10 +26352,10 @@ ${where.join("\n")}`);
26006
26352
  },
26007
26353
  prepare: async (path) => {
26008
26354
  for (const f of repoP.config.worktrees.copy) {
26009
- const src = join48(repoP.dir, f);
26355
+ const src = join49(repoP.dir, f);
26010
26356
  if (!existsSync45(src)) continue;
26011
- mkdirSync29(dirOf2(join48(path, f)), { recursive: true });
26012
- copyFileSync2(src, join48(path, f));
26357
+ mkdirSync30(dirOf2(join49(path, f)), { recursive: true });
26358
+ copyFileSync2(src, join49(path, f));
26013
26359
  }
26014
26360
  const setup = repoP.config.hooks.setup;
26015
26361
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -26283,9 +26629,9 @@ ${where.join("\n")}`);
26283
26629
  } };
26284
26630
  const store = resetStore;
26285
26631
  const h = hostFromUrl4(r.baseUrl);
26286
- const [slug] = splitRoute(r.route);
26287
- const session = await store.get(h, slug);
26288
- return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
26632
+ const [slug2] = splitRoute(r.route);
26633
+ const session = await store.get(h, slug2);
26634
+ return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug2) };
26289
26635
  },
26290
26636
  async hostSessions(stateDir) {
26291
26637
  const store = resetStore;
@@ -26304,7 +26650,7 @@ ${where.join("\n")}`);
26304
26650
  };
26305
26651
  const droppedRoutes = [];
26306
26652
  try {
26307
- const rawCfg = parseYaml2(readFileSync39(cfg.configFile, "utf8"));
26653
+ const rawCfg = parseYaml2(readFileSync40(cfg.configFile, "utf8"));
26308
26654
  for (const r of rawCfg?.routes ?? []) {
26309
26655
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
26310
26656
  try {
@@ -26357,8 +26703,8 @@ ${where.join("\n")}`);
26357
26703
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
26358
26704
  break;
26359
26705
  }
26360
- mkdirSync29(cfg.ship.stateDir, { recursive: true });
26361
- writeFileSync32(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26706
+ mkdirSync30(cfg.ship.stateDir, { recursive: true });
26707
+ writeFileSync33(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26362
26708
  `);
26363
26709
  process.stdout.write(
26364
26710
  "crew reload: requested \u2014 the daemon relaunches with the new config once its running agents end (it starts no new work meanwhile); if it is busy, `crew reload --now` forces the relaunch and interrupts them\n"
@@ -26411,7 +26757,7 @@ ${where.join("\n")}`);
26411
26757
  }
26412
26758
  case "log": {
26413
26759
  try {
26414
- const text2 = readFileSync39(cfg.ship.logFile, "utf8").split("\n");
26760
+ const text2 = readFileSync40(cfg.ship.logFile, "utf8").split("\n");
26415
26761
  process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
26416
26762
  `);
26417
26763
  } catch {
@@ -26434,7 +26780,7 @@ ${where.join("\n")}`);
26434
26780
  };
26435
26781
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
26436
26782
  const tracker = new Tracker(route2, cfg.ship);
26437
- const configText = readFileSync39(cfg.configFile, "utf8");
26783
+ const configText = readFileSync40(cfg.configFile, "utf8");
26438
26784
  const cfgBase = dirname25(cfg.configFile);
26439
26785
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
26440
26786
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
@@ -26463,7 +26809,7 @@ ${where.join("\n")}`);
26463
26809
  }
26464
26810
  if (configured2.some((r) => r.dir === dir)) {
26465
26811
  try {
26466
- writeFileSync32(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26812
+ writeFileSync33(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26467
26813
  } catch (e) {
26468
26814
  if (e instanceof ReposError) fail(e.message);
26469
26815
  throw e;
@@ -26472,28 +26818,28 @@ ${where.join("\n")}`);
26472
26818
  if (row) {
26473
26819
  await tracker.deleteRepoRow(row.id);
26474
26820
  if (existsSync45(resolvedFile)) {
26475
- const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26821
+ const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
26476
26822
  if (raw.repoNames) delete raw.repoNames[row.id];
26477
26823
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
26478
- writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
26824
+ writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
26479
26825
  `);
26480
26826
  }
26481
26827
  }
26482
- if (existsSync45(join48(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync22(dir, { recursive: true, force: true });
26828
+ if (existsSync45(join49(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
26483
26829
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
26484
26830
  break;
26485
26831
  }
26486
26832
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
26487
- const fresh = !existsSync45(join48(dir, ".git"));
26833
+ const fresh = !existsSync45(join49(dir, ".git"));
26488
26834
  if (existsSync45(dir) && fresh && readdirSync20(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
26489
26835
  if (dryRun) {
26490
26836
  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`);
26491
26837
  break;
26492
26838
  }
26493
26839
  if (fresh) {
26494
- mkdirSync29(dir, { recursive: true });
26495
- writeFileSync32(join48(dir, "README.md"), SAMPLE_README);
26496
- writeFileSync32(join48(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
26840
+ mkdirSync30(dir, { recursive: true });
26841
+ writeFileSync33(join49(dir, "README.md"), SAMPLE_README);
26842
+ writeFileSync33(join49(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
26497
26843
  const g = (...a) => {
26498
26844
  const r = spawnSync10("git", a, { cwd: dir, encoding: "utf8" });
26499
26845
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -26512,16 +26858,16 @@ ${where.join("\n")}`);
26512
26858
  });
26513
26859
  repoId = (await tracker.createRepoRow(payload)).id;
26514
26860
  if (existsSync45(resolvedFile)) {
26515
- const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26861
+ const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
26516
26862
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
26517
26863
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
26518
- writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
26864
+ writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
26519
26865
  `);
26520
26866
  }
26521
26867
  }
26522
26868
  if (!configured2.some((r) => r.dir === dir)) {
26523
26869
  try {
26524
- writeFileSync32(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
26870
+ writeFileSync33(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
26525
26871
  } catch (e) {
26526
26872
  if (e instanceof ReposError) fail(e.message);
26527
26873
  throw e;
@@ -26964,7 +27310,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
26964
27310
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
26965
27311
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
26966
27312
  try {
26967
- return readFileSync39(p, "utf8");
27313
+ return readFileSync40(p, "utf8");
26968
27314
  } catch {
26969
27315
  return null;
26970
27316
  }
@@ -27386,13 +27732,13 @@ ${foreign.join("\n")}
27386
27732
  if (cfg.ship.relayHost) {
27387
27733
  for (const r of passengerRoutes) {
27388
27734
  const ws = r.resolved?.workspaceId;
27389
- const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
27735
+ const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
27390
27736
  if (!ws) continue;
27391
27737
  process.stdout.write(
27392
- ` ${r.route} url: ${slug ? publicUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain) : "not minted yet (first tunnel cycle creates it)"}
27738
+ ` ${r.route} url: ${slug2 ? publicUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain) : "not minted yet (first tunnel cycle creates it)"}
27393
27739
  `
27394
27740
  );
27395
- const controlUrl = slug && readPersistedTunnel(cfg.ship.stateDir, ws)?.controlSocket ? `${relayBaseUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control` : void 0;
27741
+ const controlUrl = slug2 && readPersistedTunnel(cfg.ship.stateDir, ws)?.controlSocket ? `${relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control` : void 0;
27396
27742
  process.stdout.write(` ${r.route} control url: ${controlUrl ?? "none (the tunnel carries passengers only)"}
27397
27743
  `);
27398
27744
  const api = endpointApiFor([r])(ws);
@@ -27411,6 +27757,15 @@ ${foreign.join("\n")}
27411
27757
  );
27412
27758
  }
27413
27759
  }
27760
+ {
27761
+ const declaredS = await sandboxReposOf(cfg.routes.filter((r) => r.enabled));
27762
+ const stateS = readSandboxState(cfg.ship.stateDir);
27763
+ for (const r of declaredS) {
27764
+ const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl));
27765
+ process.stdout.write(`sandbox image: ${r.name}: ${st.status.toUpperCase()} \u2014 ${st.detail}
27766
+ `);
27767
+ }
27768
+ }
27414
27769
  for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
27415
27770
  const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
27416
27771
  process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret}, attached only)
@@ -27469,3 +27824,4 @@ var endpointReconcileDeps2;
27469
27824
  var releaseRefused2;
27470
27825
  var doctorMachineId2;
27471
27826
  var baseHealth2;
27827
+ var sandboxTickDeps2;