@tablation/crew 0.103.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 +1491 -895
  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
@@ -2678,6 +2714,23 @@ function parseCloneScheme(v) {
2678
2714
  if (v === "ssh" || v === "https") return v;
2679
2715
  throw new ConfigError(`ship.cloneScheme must be "ssh" or "https", got ${JSON.stringify(v)}`);
2680
2716
  }
2717
+ function parseShipRunner(v) {
2718
+ if (v === void 0 || v === null || v === "") return void 0;
2719
+ if (v === "host" || v === "sandbox") return v;
2720
+ throw new ConfigError(`ship.runner must be "host" or "sandbox", got ${JSON.stringify(v)}`);
2721
+ }
2722
+ var SANDBOX_KEYS = ["bin", "image", "policy", "cacheVolume", "mirrorDir", "workDir", "agentBin", "forgeTokenVar"];
2723
+ function parseShipSandbox(v) {
2724
+ if (v === void 0 || v === null) return void 0;
2725
+ if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.sandbox must be a map of settings");
2726
+ const out = {};
2727
+ for (const [k, val] of Object.entries(v)) {
2728
+ if (!SANDBOX_KEYS.includes(k)) throw new ConfigError(`ship.sandbox: unknown setting "${k}" (known: ${SANDBOX_KEYS.join(", ")})`);
2729
+ if (typeof val !== "string" || !val) throw new ConfigError(`ship.sandbox.${k} must be a non-empty string`);
2730
+ out[k] = val;
2731
+ }
2732
+ return out;
2733
+ }
2681
2734
  function parseShipEnv(v) {
2682
2735
  if (v === void 0 || v === null) return void 0;
2683
2736
  if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.env must be a map of variable name to value");
@@ -3124,6 +3177,8 @@ Create the first of those:
3124
3177
  if (dupe) missing.add(`routes: duplicate route "${dupe}" \u2014 a route addresses itself, so it must be unique`);
3125
3178
  missing.throwIfAny(file);
3126
3179
  configureSecretStore({ mode: secretStore, stateDir });
3180
+ const shipRunner = parseShipRunner(shipRaw.runner);
3181
+ const shipSandbox = parseShipSandbox(shipRaw.sandbox);
3127
3182
  return {
3128
3183
  ship: {
3129
3184
  name: shipRaw.name ?? "this ship",
@@ -3133,6 +3188,7 @@ Create the first of those:
3133
3188
  model: shipRaw.agent?.model ?? "claude-sonnet-5",
3134
3189
  maxThinkingTokens
3135
3190
  },
3191
+ ...shipRunner ? { runner: shipRunner, ...shipSandbox ? { sandbox: shipSandbox } : {} } : {},
3136
3192
  ...toolchains.length > 0 ? { toolchains } : {},
3137
3193
  ...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
3138
3194
  ...pair ? { pair } : {},
@@ -3844,9 +3900,9 @@ function labelFor(crewHome, job) {
3844
3900
  return crewHome;
3845
3901
  }
3846
3902
  })();
3847
- const hash = createHash3("sha1").update(real).digest("hex").slice(0, 8);
3903
+ const hash2 = createHash3("sha1").update(real).digest("hex").slice(0, 8);
3848
3904
  const stem = job === "release" ? "com.tablation.crew-release" : job === "passengers" ? "com.tablation.crew-passengers" : "com.tablation.crew";
3849
- return `${stem}.${hash}`;
3905
+ return `${stem}.${hash2}`;
3850
3906
  }
3851
3907
  function cronMarkerFor(label) {
3852
3908
  return `# crew:${label} \u2014 managed by \`crew install\`, do not edit by hand`;
@@ -5293,11 +5349,433 @@ function attentionProblems(items, already) {
5293
5349
  return items.filter((i) => !seen.has(i.key)).map((i) => ({ severity: "attention", kind: i.kind, message: i.message }));
5294
5350
  }
5295
5351
 
5296
- // 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
5297
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";
5298
5776
  var PROBE_BRANCH = "crew-probe-branch";
5299
5777
  var realGit = (cwd, args, env) => {
5300
- execFileSync3("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5778
+ execFileSync4("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5301
5779
  };
5302
5780
  function isSshRemote(url) {
5303
5781
  return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
@@ -5305,12 +5783,12 @@ function isSshRemote(url) {
5305
5783
  function pushAuthHint(remoteUrl) {
5306
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`";
5307
5785
  }
5308
- function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
5786
+ function probePushAuth(o, run3 = realGit, baseEnv = process.env) {
5309
5787
  if (o.offline) return { state: "skipped", detail: "skipped (--offline)" };
5310
5788
  if (!o.remoteUrl) return { state: "skipped", detail: `no remote "${o.remote}" configured` };
5311
5789
  const env = { ...baseEnv, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_SSH_COMMAND: `${baseEnv.GIT_SSH_COMMAND ?? "ssh"} -o BatchMode=yes` };
5312
5790
  try {
5313
- 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);
5314
5792
  return { state: "ok" };
5315
5793
  } catch (e) {
5316
5794
  const err3 = e;
@@ -5326,7 +5804,7 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
5326
5804
  };
5327
5805
  }
5328
5806
  try {
5329
- run2(o.dir, ["ls-remote", "--exit-code", o.remote, "HEAD"], env);
5807
+ run3(o.dir, ["ls-remote", "--exit-code", o.remote, "HEAD"], env);
5330
5808
  return {
5331
5809
  state: "failed",
5332
5810
  detail: `the token for forge ${f.host} is read-only (fetch works, push refused): ${detail}`,
@@ -5418,31 +5896,31 @@ function roleCapWarnings(ship) {
5418
5896
  }
5419
5897
 
5420
5898
  // src/selftest.ts
5421
- import { spawn as spawn3 } from "child_process";
5422
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, mkdtempSync, readFileSync as readFileSync20, rmSync as rmSync12, writeFileSync as writeFileSync14 } from "fs";
5423
- import { tmpdir as tmpdir2 } from "os";
5424
- import { join as join23 } 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";
5425
5903
 
5426
5904
  // src/agent.ts
5427
- import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
5428
- import { readFileSync as readFileSync19, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5905
+ import { spawnSync as spawnSync5 } from "child_process";
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";
5429
5907
  import { StringDecoder } from "string_decoder";
5430
5908
  import { finished } from "stream/promises";
5431
- import { join as join22, dirname as dirname8, basename as basename7 } from "path";
5432
- 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";
5433
5911
 
5434
5912
  // src/state.ts
5435
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9, rmSync as rmSync9, statSync as statSync6, readdirSync as readdirSync5 } from "fs";
5436
- 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";
5437
5915
  var EPOCH = "1970-01-01T00:00:00.000Z";
5438
5916
  var State = class _State {
5439
5917
  dir;
5440
5918
  constructor(dir) {
5441
5919
  this.dir = dir;
5442
- mkdirSync9(dir, { recursive: true });
5920
+ mkdirSync10(dir, { recursive: true });
5443
5921
  }
5444
5922
  path(...parts) {
5445
- return join15(this.dir, ...parts);
5923
+ return join16(this.dir, ...parts);
5446
5924
  }
5447
5925
  /**
5448
5926
  * The timestamp every "new since last poll" check is measured against.
@@ -5453,14 +5931,14 @@ var State = class _State {
5453
5931
  */
5454
5932
  watermark() {
5455
5933
  try {
5456
- const v = readFileSync14(this.path(".poll-watermark"), "utf8").trim();
5934
+ const v = readFileSync15(this.path(".poll-watermark"), "utf8").trim();
5457
5935
  return v || EPOCH;
5458
5936
  } catch {
5459
5937
  return EPOCH;
5460
5938
  }
5461
5939
  }
5462
5940
  setWatermark(at) {
5463
- writeFileSync10(this.path(".poll-watermark"), `${at}
5941
+ writeFileSync11(this.path(".poll-watermark"), `${at}
5464
5942
  `);
5465
5943
  }
5466
5944
  /**
@@ -5487,10 +5965,10 @@ var State = class _State {
5487
5965
  return new Set(roles.filter((r) => this.isRolePaused(r)));
5488
5966
  }
5489
5967
  pause(role) {
5490
- writeFileSync10(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5968
+ writeFileSync11(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
5491
5969
  }
5492
5970
  resume(role) {
5493
- 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 });
5494
5972
  }
5495
5973
  /**
5496
5974
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -5501,8 +5979,8 @@ var State = class _State {
5501
5979
  return existsSync14(this.path(".release-paused"));
5502
5980
  }
5503
5981
  setReleasePaused(paused) {
5504
- if (paused) writeFileSync10(this.path(".release-paused"), "");
5505
- 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 });
5506
5984
  }
5507
5985
  /**
5508
5986
  * Why a release cycle must not run right now, or null when it may. Every
@@ -5539,7 +6017,7 @@ var State = class _State {
5539
6017
  const name = _State.safe(rawName);
5540
6018
  const file = this.path(`.${name}.lock`);
5541
6019
  try {
5542
- const existing = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
6020
+ const existing = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
5543
6021
  if (Number.isFinite(existing) && existing > 0) {
5544
6022
  try {
5545
6023
  process.kill(existing, 0);
@@ -5549,13 +6027,13 @@ var State = class _State {
5549
6027
  }
5550
6028
  } catch {
5551
6029
  }
5552
- writeFileSync10(file, `${process.pid}
6030
+ writeFileSync11(file, `${process.pid}
5553
6031
  `);
5554
6032
  return {
5555
6033
  ok: true,
5556
6034
  release: () => {
5557
6035
  try {
5558
- rmSync9(file, { force: true });
6036
+ rmSync10(file, { force: true });
5559
6037
  } catch {
5560
6038
  }
5561
6039
  }
@@ -5577,7 +6055,7 @@ var State = class _State {
5577
6055
  if (!/^\.release-.+\.lock$/.test(n)) continue;
5578
6056
  let pid = 0;
5579
6057
  try {
5580
- pid = Number.parseInt(readFileSync14(this.path(n), "utf8").trim(), 10);
6058
+ pid = Number.parseInt(readFileSync15(this.path(n), "utf8").trim(), 10);
5581
6059
  } catch {
5582
6060
  continue;
5583
6061
  }
@@ -5663,7 +6141,7 @@ var State = class _State {
5663
6141
  ticketInFlight(recordId) {
5664
6142
  const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
5665
6143
  try {
5666
- const pid = Number.parseInt(readFileSync14(file, "utf8").trim(), 10);
6144
+ const pid = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
5667
6145
  if (!Number.isFinite(pid) || pid <= 0) return false;
5668
6146
  process.kill(pid, 0);
5669
6147
  return true;
@@ -5697,7 +6175,7 @@ var State = class _State {
5697
6175
  const heldReported = `.release-held-${suffix}`;
5698
6176
  const read = (f) => {
5699
6177
  try {
5700
- return readFileSync14(this.path(f), "utf8").trim() || null;
6178
+ return readFileSync15(this.path(f), "utf8").trim() || null;
5701
6179
  } catch {
5702
6180
  return null;
5703
6181
  }
@@ -5706,12 +6184,12 @@ var State = class _State {
5706
6184
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
5707
6185
  noteBlocked: () => {
5708
6186
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
5709
- writeFileSync10(this.path(blocked), `${n}
6187
+ writeFileSync11(this.path(blocked), `${n}
5710
6188
  `);
5711
6189
  return n;
5712
6190
  },
5713
6191
  clearBlocked: () => {
5714
- rmSync9(this.path(blocked), { force: true });
6192
+ rmSync10(this.path(blocked), { force: true });
5715
6193
  },
5716
6194
  /**
5717
6195
  * The commit a deploy last failed on.
@@ -5723,11 +6201,11 @@ var State = class _State {
5723
6201
  */
5724
6202
  deployFailedSha: () => read(failed),
5725
6203
  noteDeployFailed: (sha) => {
5726
- writeFileSync10(this.path(failed), `${sha}
6204
+ writeFileSync11(this.path(failed), `${sha}
5727
6205
  `);
5728
6206
  },
5729
6207
  clearDeployFailed: () => {
5730
- rmSync9(this.path(failed), { force: true });
6208
+ rmSync10(this.path(failed), { force: true });
5731
6209
  },
5732
6210
  /**
5733
6211
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -5737,31 +6215,31 @@ var State = class _State {
5737
6215
  noteTestGateFailed: (sha) => {
5738
6216
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
5739
6217
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
5740
- writeFileSync10(this.path(gateFailed), `${sha} ${next}
6218
+ writeFileSync11(this.path(gateFailed), `${sha} ${next}
5741
6219
  `);
5742
6220
  return next;
5743
6221
  },
5744
6222
  clearTestGateFailed: () => {
5745
- rmSync9(this.path(gateFailed), { force: true });
6223
+ rmSync10(this.path(gateFailed), { force: true });
5746
6224
  },
5747
6225
  /** Whether the board was already told about this head's red gate. */
5748
6226
  heldReported: () => {
5749
6227
  try {
5750
- return readFileSync14(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
6228
+ return readFileSync15(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5751
6229
  } catch {
5752
6230
  return null;
5753
6231
  }
5754
6232
  },
5755
6233
  noteHeldReported: (signature) => {
5756
- writeFileSync10(this.path(heldReported), `${signature}
6234
+ writeFileSync11(this.path(heldReported), `${signature}
5757
6235
  `);
5758
6236
  },
5759
6237
  clearHeldReported: () => {
5760
- rmSync9(this.path(heldReported), { force: true });
6238
+ rmSync10(this.path(heldReported), { force: true });
5761
6239
  },
5762
6240
  testGateReported: (sha) => read(gateReported) === sha,
5763
6241
  noteTestGateReported: (sha) => {
5764
- writeFileSync10(this.path(gateReported), `${sha}
6242
+ writeFileSync11(this.path(gateReported), `${sha}
5765
6243
  `);
5766
6244
  }
5767
6245
  };
@@ -5784,13 +6262,13 @@ var State = class _State {
5784
6262
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
5785
6263
  const read = (f) => {
5786
6264
  try {
5787
- return readFileSync14(this.path(f), "utf8").trim() || null;
6265
+ return readFileSync15(this.path(f), "utf8").trim() || null;
5788
6266
  } catch {
5789
6267
  return null;
5790
6268
  }
5791
6269
  };
5792
6270
  const clear2 = () => {
5793
- 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 });
5794
6272
  };
5795
6273
  return {
5796
6274
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -5817,18 +6295,18 @@ var State = class _State {
5817
6295
  clear2();
5818
6296
  return;
5819
6297
  }
5820
- writeFileSync10(this.path(streakFile), `${this.readInt(streakFile) + 1}
6298
+ writeFileSync11(this.path(streakFile), `${this.readInt(streakFile) + 1}
5821
6299
  `);
5822
6300
  const prior = read(ticketFile);
5823
6301
  if (topTicket && topTicket === prior) {
5824
- writeFileSync10(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
6302
+ writeFileSync11(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5825
6303
  `);
5826
6304
  } else if (topTicket) {
5827
- writeFileSync10(this.path(ticketFile), `${topTicket}
6305
+ writeFileSync11(this.path(ticketFile), `${topTicket}
5828
6306
  `);
5829
- writeFileSync10(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
6307
+ writeFileSync11(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5830
6308
  `);
5831
- writeFileSync10(this.path(ticketStreakFile), "1\n");
6309
+ writeFileSync11(this.path(ticketStreakFile), "1\n");
5832
6310
  }
5833
6311
  }
5834
6312
  };
@@ -5849,14 +6327,14 @@ var State = class _State {
5849
6327
  return {
5850
6328
  previous: () => {
5851
6329
  try {
5852
- const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
6330
+ const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
5853
6331
  return parsed && typeof parsed === "object" ? parsed : {};
5854
6332
  } catch {
5855
6333
  return {};
5856
6334
  }
5857
6335
  },
5858
6336
  persist: (next) => {
5859
- writeFileSync10(this.path(file), JSON.stringify(next));
6337
+ writeFileSync11(this.path(file), JSON.stringify(next));
5860
6338
  }
5861
6339
  };
5862
6340
  }
@@ -5872,14 +6350,14 @@ var State = class _State {
5872
6350
  return {
5873
6351
  previous: () => {
5874
6352
  try {
5875
- const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
6353
+ const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
5876
6354
  return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
5877
6355
  } catch {
5878
6356
  return [];
5879
6357
  }
5880
6358
  },
5881
6359
  persist: (next) => {
5882
- writeFileSync10(this.path(file), JSON.stringify(next));
6360
+ writeFileSync11(this.path(file), JSON.stringify(next));
5883
6361
  }
5884
6362
  };
5885
6363
  }
@@ -5893,14 +6371,14 @@ var State = class _State {
5893
6371
  return {
5894
6372
  previous: () => {
5895
6373
  try {
5896
- const parsed = JSON.parse(readFileSync14(this.path(file), "utf8"));
6374
+ const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
5897
6375
  return Array.isArray(parsed) ? parsed : [];
5898
6376
  } catch {
5899
6377
  return [];
5900
6378
  }
5901
6379
  },
5902
6380
  persist: (next) => {
5903
- writeFileSync10(this.path(file), JSON.stringify(next));
6381
+ writeFileSync11(this.path(file), JSON.stringify(next));
5904
6382
  }
5905
6383
  };
5906
6384
  }
@@ -5913,7 +6391,7 @@ var State = class _State {
5913
6391
  const file = ".release-last-run";
5914
6392
  return {
5915
6393
  touch: () => {
5916
- writeFileSync10(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
6394
+ writeFileSync11(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5917
6395
  },
5918
6396
  at: () => {
5919
6397
  try {
@@ -5926,7 +6404,7 @@ var State = class _State {
5926
6404
  }
5927
6405
  readInt(file) {
5928
6406
  try {
5929
- return Number.parseInt(readFileSync14(this.path(file), "utf8").trim(), 10) || 0;
6407
+ return Number.parseInt(readFileSync15(this.path(file), "utf8").trim(), 10) || 0;
5930
6408
  } catch {
5931
6409
  return 0;
5932
6410
  }
@@ -6409,8 +6887,8 @@ var PERISCOPE_COLUMNS = [
6409
6887
  ];
6410
6888
 
6411
6889
  // src/agent-log.ts
6412
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
6413
- 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";
6414
6892
 
6415
6893
  // src/pending-backoff.ts
6416
6894
  var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
@@ -6467,14 +6945,14 @@ async function findTicketRecord(target, t, ref) {
6467
6945
  );
6468
6946
  return rows.length === 1 ? rows[0].id : void 0;
6469
6947
  }
6470
- function counterIncrements(run2, cols, have) {
6948
+ function counterIncrements(run3, cols, have) {
6471
6949
  const figures = [
6472
6950
  [cols.agentRuns, 1],
6473
- [cols.tokensIn, run2.tokensIn],
6474
- [cols.tokensOut, run2.tokensOut],
6475
- [cols.cacheReadTokens, run2.cacheReadTokens],
6476
- [cols.cacheWriteTokens, run2.cacheWriteTokens],
6477
- [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]
6478
6956
  ];
6479
6957
  const out = {};
6480
6958
  for (const [column, value2] of figures) {
@@ -6497,41 +6975,41 @@ async function post(target, path, body) {
6497
6975
  if (!res.ok) throw new AgentLogHttpError(`${path}: ${res.status} ${res.statusText}`, res.status);
6498
6976
  return await res.json();
6499
6977
  }
6500
- async function reportAgentRun(target, run2) {
6978
+ async function reportAgentRun(target, run3) {
6501
6979
  let ticketRecordId;
6502
- if (target.tickets && run2.ticketReference) {
6980
+ if (target.tickets && run3.ticketReference) {
6503
6981
  try {
6504
- ticketRecordId = await findTicketRecord(target, target.tickets, run2.ticketReference);
6982
+ ticketRecordId = await findTicketRecord(target, target.tickets, run3.ticketReference);
6505
6983
  } catch {
6506
6984
  }
6507
6985
  }
6508
6986
  const body = {
6509
6987
  agentId: target.agentId,
6510
- ticketReference: run2.ticketReference,
6511
- outcome: run2.outcome,
6512
- startedAt: run2.startedAt,
6513
- finishedAt: run2.finishedAt,
6988
+ ticketReference: run3.ticketReference,
6989
+ outcome: run3.outcome,
6990
+ startedAt: run3.startedAt,
6991
+ finishedAt: run3.finishedAt,
6514
6992
  source: "client",
6515
6993
  client: "crew",
6516
- model: run2.model,
6517
- tokensIn: run2.tokensIn,
6518
- tokensOut: run2.tokensOut,
6519
- cacheReadTokens: run2.cacheReadTokens,
6520
- cacheWriteTokens: run2.cacheWriteTokens,
6521
- costUsd: run2.costUsd,
6522
- numTurns: run2.numTurns,
6523
- prefixTokens: run2.prefixTokens,
6524
- promptVersion: run2.promptVersion,
6525
- 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
6526
7004
  };
6527
7005
  const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
6528
7006
  let entry;
6529
7007
  try {
6530
- 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 });
6531
7009
  } catch (e) {
6532
7010
  if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
6533
7011
  if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
6534
- if (e.status !== 400 || !run2.harness && !run2.provider && !ticketRecordId) {
7012
+ if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId) {
6535
7013
  e.row = true;
6536
7014
  throw e;
6537
7015
  }
@@ -6542,7 +7020,7 @@ async function reportAgentRun(target, run2) {
6542
7020
  throw e2;
6543
7021
  }
6544
7022
  }
6545
- for (const cycle of run2.cycles) {
7023
+ for (const cycle of run3.cycles) {
6546
7024
  await post(target, `/agents/log/${entry.id}/cycles`, {
6547
7025
  cycleIndex: cycle.cycleIndex,
6548
7026
  occurredAt: cycle.occurredAt,
@@ -6550,7 +7028,7 @@ async function reportAgentRun(target, run2) {
6550
7028
  ...cycle.role ? { role: cycle.role } : {}
6551
7029
  });
6552
7030
  }
6553
- 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;
6554
7032
  return {
6555
7033
  agentLogId: entry.id,
6556
7034
  ...counters?.warning ? { counterWarning: counters.warning } : {},
@@ -6564,38 +7042,38 @@ async function writeCounters(target, w) {
6564
7042
  if (Object.keys(body).length === 0) return;
6565
7043
  await request(target, "PATCH", `/data-models/${w.issuesModelId}/records/${w.recordId}`, body);
6566
7044
  }
6567
- async function addRunToTicket(target, t, recordId, run2) {
7045
+ async function addRunToTicket(target, t, recordId, run3) {
6568
7046
  const write = {
6569
7047
  issuesModelId: t.issuesModelId,
6570
7048
  recordId,
6571
- ticketReference: run2.ticketReference,
7049
+ ticketReference: run3.ticketReference,
6572
7050
  counters: t.counters,
6573
- body: counterIncrements(run2, t.counters, new Set(Object.values(t.counters)))
7051
+ body: counterIncrements(run3, t.counters, new Set(Object.values(t.counters)))
6574
7052
  };
6575
7053
  try {
6576
7054
  await writeCounters(target, write);
6577
7055
  return {};
6578
7056
  } catch (e) {
6579
7057
  return {
6580
- warning: `could not add run spend to ${run2.ticketReference}: ${e.message}`,
7058
+ warning: `could not add run spend to ${run3.ticketReference}: ${e.message}`,
6581
7059
  ...isRetryable(e) ? { retry: write } : {}
6582
7060
  };
6583
7061
  }
6584
7062
  }
6585
- var queueDir = (stateDir) => join16(stateDir, "pending-agent-logs");
7063
+ var queueDir = (stateDir) => join17(stateDir, "pending-agent-logs");
6586
7064
  function queueAgentRun(stateDir, name, q) {
6587
- mkdirSync10(queueDir(stateDir), { recursive: true });
6588
- 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));
6589
7067
  }
6590
7068
  async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6591
7069
  const dir = queueDir(stateDir);
6592
7070
  if (!existsSync15(dir)) return 0;
6593
7071
  let sent = 0;
6594
7072
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
6595
- const path = join16(dir, f);
7073
+ const path = join17(dir, f);
6596
7074
  let q;
6597
7075
  try {
6598
- q = JSON.parse(readFileSync15(path, "utf8"));
7076
+ q = JSON.parse(readFileSync16(path, "utf8"));
6599
7077
  } catch {
6600
7078
  continue;
6601
7079
  }
@@ -6614,17 +7092,17 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
6614
7092
  attempts: 0,
6615
7093
  nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
6616
7094
  });
6617
- rmSync10(path, { force: true });
7095
+ rmSync11(path, { force: true });
6618
7096
  sent += 1;
6619
7097
  log.emit(`sent a queued Agent Log report (${f})`);
6620
7098
  } catch (e) {
6621
7099
  if (!(e instanceof AgentLogUnreachableError)) {
6622
- rmSync10(path, { force: true });
7100
+ rmSync11(path, { force: true });
6623
7101
  log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
6624
7102
  continue;
6625
7103
  }
6626
7104
  const attempts = q.attempts + 1;
6627
- writeFileSync11(path, JSON.stringify({
7105
+ writeFileSync12(path, JSON.stringify({
6628
7106
  ...q,
6629
7107
  attempts,
6630
7108
  warned: true,
@@ -6635,20 +7113,20 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
6635
7113
  }
6636
7114
  return sent;
6637
7115
  }
6638
- var countersDir = (stateDir) => join16(stateDir, "pending-ticket-counters");
7116
+ var countersDir = (stateDir) => join17(stateDir, "pending-ticket-counters");
6639
7117
  function queueTicketCounters(stateDir, name, q) {
6640
- mkdirSync10(countersDir(stateDir), { recursive: true });
6641
- 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));
6642
7120
  }
6643
7121
  async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6644
7122
  const dir = countersDir(stateDir);
6645
7123
  if (!existsSync15(dir)) return 0;
6646
7124
  let sent = 0;
6647
7125
  for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
6648
- const path = join16(dir, f);
7126
+ const path = join17(dir, f);
6649
7127
  let q;
6650
7128
  try {
6651
- q = JSON.parse(readFileSync15(path, "utf8"));
7129
+ q = JSON.parse(readFileSync16(path, "utf8"));
6652
7130
  } catch {
6653
7131
  continue;
6654
7132
  }
@@ -6657,17 +7135,17 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
6657
7135
  const apiKey = await keyFor(q.role);
6658
7136
  if (!apiKey) continue;
6659
7137
  await writeCounters({ ...q.target, apiKey }, q.write);
6660
- rmSync10(path, { force: true });
7138
+ rmSync11(path, { force: true });
6661
7139
  sent += 1;
6662
7140
  log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
6663
7141
  } catch (e) {
6664
7142
  if (!isRetryable(e)) {
6665
- rmSync10(path, { force: true });
7143
+ rmSync11(path, { force: true });
6666
7144
  log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
6667
7145
  continue;
6668
7146
  }
6669
7147
  const attempts = q.attempts + 1;
6670
- writeFileSync11(path, JSON.stringify({
7148
+ writeFileSync12(path, JSON.stringify({
6671
7149
  ...q,
6672
7150
  attempts,
6673
7151
  warned: true,
@@ -6870,8 +7348,8 @@ var HandoffTranscriptTracker = class {
6870
7348
  };
6871
7349
 
6872
7350
  // src/pending-handoff.ts
6873
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync16, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "fs";
6874
- 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";
6875
7353
 
6876
7354
  // src/ticket-branch.ts
6877
7355
  import { basename as basename6 } from "path";
@@ -8038,15 +8516,15 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
8038
8516
 
8039
8517
  // src/run-worktree.ts
8040
8518
  import { existsSync as existsSync17 } from "fs";
8041
- import { dirname as dirname7, join as join18 } from "path";
8519
+ import { dirname as dirname7, join as join19 } from "path";
8042
8520
 
8043
8521
  // src/ports.ts
8044
- import { execFileSync as execFileSync4 } from "child_process";
8522
+ import { execFileSync as execFileSync5 } from "child_process";
8045
8523
  import { existsSync as existsSync16 } from "fs";
8046
- import { join as join17 } from "path";
8047
- var run = (cmd, args) => {
8524
+ import { join as join18 } from "path";
8525
+ var run2 = (cmd, args) => {
8048
8526
  try {
8049
- return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8527
+ return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8050
8528
  } catch {
8051
8529
  return "";
8052
8530
  }
@@ -8060,7 +8538,7 @@ function listeners() {
8060
8538
  seen.add(key);
8061
8539
  out.push({ pid, port });
8062
8540
  };
8063
- const lsof = run("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
8541
+ const lsof = run2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
8064
8542
  if (lsof) {
8065
8543
  for (const line of lsof.split("\n").slice(1)) {
8066
8544
  const cols = line.split(/\s+/);
@@ -8070,7 +8548,7 @@ function listeners() {
8070
8548
  }
8071
8549
  return out;
8072
8550
  }
8073
- const ss = run("ss", ["-ltnpH"]);
8551
+ const ss = run2("ss", ["-ltnpH"]);
8074
8552
  for (const line of ss.split("\n")) {
8075
8553
  const local = line.trim().split(/\s+/)[3];
8076
8554
  const pid = /pid=(\d+)/.exec(line)?.[1];
@@ -8088,7 +8566,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
8088
8566
  }
8089
8567
  function worktreeExists(parent, prefix, n) {
8090
8568
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
8091
- if (existsSync16(join17(parent, name, ".git"))) return true;
8569
+ if (existsSync16(join18(parent, name, ".git"))) return true;
8092
8570
  }
8093
8571
  return false;
8094
8572
  }
@@ -8122,7 +8600,7 @@ async function killGently(pids, graceMs = 2e3) {
8122
8600
  function pidsInWorktree(path, n, s = DEFAULT_PORTS) {
8123
8601
  const pids = /* @__PURE__ */ new Set();
8124
8602
  for (const port of [s.backendBase + n, s.frontendBase + n]) for (const p of pidsOnPort(port)) pids.add(p);
8125
- const matched = run("pgrep", ["-f", `${path}/`]);
8603
+ const matched = run2("pgrep", ["-f", `${path}/`]);
8126
8604
  for (const line of matched.split("\n")) {
8127
8605
  const pid = Number(line.trim());
8128
8606
  if (Number.isFinite(pid) && pid > 0 && pid !== process.pid) pids.add(pid);
@@ -8187,7 +8665,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
8187
8665
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
8188
8666
  return { action: "recut", path: existing.path, branch, base: baseRef };
8189
8667
  }
8190
- const path = join18(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8668
+ const path = join19(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8191
8669
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
8192
8670
  return { action: "cut-remote", path, branch: located.branch };
8193
8671
  }
@@ -8644,12 +9122,12 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
8644
9122
 
8645
9123
  // src/pending-handoff.ts
8646
9124
  var PENDING_MAX_AGE_MS = 48 * 36e5;
8647
- var pendingDirFor = (stateDir) => join19(stateDir, "pending-handoffs");
8648
- var stateDirOfStream = (streamPath) => join19(streamPath, "..", "..");
9125
+ var pendingDirFor = (stateDir) => join20(stateDir, "pending-handoffs");
9126
+ var stateDirOfStream = (streamPath) => join20(streamPath, "..", "..");
8649
9127
  function writePendingHandoff(stateDir, rec) {
8650
9128
  const dir = pendingDirFor(stateDir);
8651
- mkdirSync11(dir, { recursive: true });
8652
- 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));
8653
9131
  }
8654
9132
  function listPendingHandoffs(stateDir, route2) {
8655
9133
  const dir = pendingDirFor(stateDir);
@@ -8657,7 +9135,7 @@ function listPendingHandoffs(stateDir, route2) {
8657
9135
  const out = [];
8658
9136
  for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
8659
9137
  try {
8660
- const rec = JSON.parse(readFileSync16(join19(dir, f), "utf8"));
9138
+ const rec = JSON.parse(readFileSync17(join20(dir, f), "utf8"));
8661
9139
  if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
8662
9140
  } catch {
8663
9141
  }
@@ -8665,7 +9143,7 @@ function listPendingHandoffs(stateDir, route2) {
8665
9143
  return out;
8666
9144
  }
8667
9145
  function removePendingHandoff(stateDir, ticketRecordId) {
8668
- rmSync11(join19(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
9146
+ rmSync12(join20(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
8669
9147
  }
8670
9148
  function pendingHandoffFor(stateDir, ticketRecordId) {
8671
9149
  return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
@@ -8775,8 +9253,8 @@ function expandSkillMarkers(text2, lookup2, opts = {}) {
8775
9253
  }
8776
9254
 
8777
9255
  // src/skills.ts
8778
- import { readFileSync as readFileSync18, readdirSync as readdirSync8, existsSync as existsSync20 } from "fs";
8779
- 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";
8780
9258
  import { parse as parseYaml } from "yaml";
8781
9259
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
8782
9260
 
@@ -8792,8 +9270,8 @@ ${additions}`;
8792
9270
  }
8793
9271
 
8794
9272
  // src/agents.ts
8795
- import { readFileSync as readFileSync17, existsSync as existsSync19 } from "fs";
8796
- import { join as join20 } from "path";
9273
+ import { readFileSync as readFileSync18, existsSync as existsSync19 } from "fs";
9274
+ import { join as join21 } from "path";
8797
9275
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
8798
9276
  var PERSONA_NAME = {
8799
9277
  dev: "Developer",
@@ -8807,12 +9285,12 @@ var CREW_AGENT_COLUMN = "agent_id";
8807
9285
  var AgentsSyncError = class extends Error {
8808
9286
  };
8809
9287
  function personaDefaultPrompt(promptsDir, role) {
8810
- const briefPath = join20(promptsDir, "personas", `lane-${role}.md`);
9288
+ const briefPath = join21(promptsDir, "personas", `lane-${role}.md`);
8811
9289
  if (!existsSync19(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8812
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync17(briefPath, "utf8");
8813
- const commonPath = join20(promptsDir, "personas", "common.md");
8814
- if (!existsSync19(commonPath)) return readFileSync17(briefPath, "utf8");
8815
- 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")}`;
8816
9294
  }
8817
9295
  async function resolveAgentId(route2, role, opts) {
8818
9296
  const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
@@ -9259,7 +9737,7 @@ function describeCrewLink(o) {
9259
9737
  var SkillSyncError = class extends Error {
9260
9738
  };
9261
9739
  function parseSkillFile(path) {
9262
- const text2 = readFileSync18(path, "utf8");
9740
+ const text2 = readFileSync19(path, "utf8");
9263
9741
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text2);
9264
9742
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
9265
9743
  const frontmatter = match[1];
@@ -9272,9 +9750,9 @@ function parseSkillFile(path) {
9272
9750
  return { name: meta.name, description: meta.description, prompt: body, file: path };
9273
9751
  }
9274
9752
  function listLocalSkills(promptsDir) {
9275
- const dir = join21(promptsDir, "skills");
9753
+ const dir = join22(promptsDir, "skills");
9276
9754
  if (!existsSync20(dir)) return [];
9277
- 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)));
9278
9756
  }
9279
9757
  async function syncSkills(route2, opts) {
9280
9758
  if (!route2.resolved) {
@@ -9457,6 +9935,13 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
9457
9935
  return out;
9458
9936
  }
9459
9937
 
9938
+ // src/runner.ts
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";
9943
+ import { promisify } from "util";
9944
+
9460
9945
  // src/run-limit.ts
9461
9946
  var DEFAULT_MAX_RUN_MINUTES = 240;
9462
9947
  var KILL_GRACE_MS = 5e3;
@@ -9506,6 +9991,186 @@ function runKilledComment(b) {
9506
9991
  The agent run on this ticket was stopped at its wall-clock limit of ${b.limitMinutes} minutes before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The worktree is left as it was. The next run on this seat resumes from it rather than starting over.`;
9507
9992
  }
9508
9993
 
9994
+ // src/runner.ts
9995
+ var execFileP = promisify(execFile);
9996
+ var DEFAULT_SANDBOX_CONFIG = {
9997
+ bin: "openshell",
9998
+ mirrorDir: "/cache",
9999
+ workDir: "/sandbox/work"
10000
+ };
10001
+ var SANDBOX_STDIN_LIMIT = 4 * 1024 * 1024 - 64 * 1024;
10002
+ var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
10003
+ var HostRunner = class {
10004
+ kind = "host";
10005
+ start(plan, o) {
10006
+ const child = spawn3(plan.bin, plan.args, {
10007
+ cwd: plan.cwd,
10008
+ env: o.env,
10009
+ stdio: ["pipe", o.streamFd, "inherit"],
10010
+ detached: true
10011
+ });
10012
+ return { child, handle: { runner: "host", pid: child.pid } };
10013
+ }
10014
+ async kill(handle) {
10015
+ if (handle.pid !== void 0) await killRunGroup(handle.pid);
10016
+ }
10017
+ isAlive(handle) {
10018
+ if (handle.pid === void 0) return false;
10019
+ try {
10020
+ process.kill(handle.pid, 0);
10021
+ return true;
10022
+ } catch (e) {
10023
+ return e.code === "EPERM";
10024
+ }
10025
+ }
10026
+ reattach(meta) {
10027
+ return { runner: "host", pid: meta.pid };
10028
+ }
10029
+ async release() {
10030
+ }
10031
+ };
10032
+ function shQuote2(s) {
10033
+ return `'${s.replace(/'/g, `'\\''`)}'`;
10034
+ }
10035
+ function sandboxName(plan, now = Date.now()) {
10036
+ const base = [plan.route, plan.role, plan.ticket ?? ""].join("-").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10037
+ return `crew-${base.slice(0, 36)}-${now.toString(36)}`.replace(/-+/g, "-");
10038
+ }
10039
+ var SandboxRunner = class {
10040
+ kind = "sandbox";
10041
+ cfg;
10042
+ /** Injected for tests; the real one reads the plan's checkout. */
10043
+ originOf;
10044
+ constructor(cfg = {}, originOf = gitOrigin) {
10045
+ this.cfg = { ...DEFAULT_SANDBOX_CONFIG, ...cfg };
10046
+ this.originOf = originOf;
10047
+ }
10048
+ os(args, o = {}) {
10049
+ return execFileP(this.cfg.bin, args, { env: o.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
10050
+ }
10051
+ async start(plan, o) {
10052
+ const origin = this.originOf(plan.cwd);
10053
+ if (!origin) throw new Error(`cannot find the origin remote of ${plan.cwd}, so there is nothing to clone into the sandbox`);
10054
+ const repoName = basename7(origin).replace(/\.git$/, "");
10055
+ const name = sandboxName(plan);
10056
+ const { cfg } = this;
10057
+ const secretKeys = Object.keys(plan.setEnv);
10058
+ if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0 && !secretKeys.includes(cfg.forgeTokenVar)) secretKeys.push(cfg.forgeTokenVar);
10059
+ const createEnv = { ...process.env, ...plan.setEnv };
10060
+ if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0) createEnv[cfg.forgeTokenVar] = o.env[cfg.forgeTokenVar];
10061
+ const create2 = ["sandbox", "create", "--name", name, "--detach"];
10062
+ if (cfg.policy) create2.push("--policy", cfg.policy);
10063
+ if (cfg.image) create2.push("--from", cfg.image);
10064
+ if (cfg.cacheVolume) {
10065
+ create2.push("--driver-config-json", JSON.stringify({
10066
+ docker: { mounts: [{ type: "volume", source: cfg.cacheVolume, target: cfg.mirrorDir, read_only: true }] }
10067
+ }));
10068
+ }
10069
+ for (const k of secretKeys) create2.push("--env-from", k);
10070
+ const maxSeconds = Math.max(60, Math.ceil((plan.maxRunMinutes ?? 0) * 60) + 600);
10071
+ create2.push("--", "sleep", String(plan.maxRunMinutes ? maxSeconds : 86400));
10072
+ const t0 = Date.now();
10073
+ try {
10074
+ await this.os(create2, { env: createEnv });
10075
+ } catch (e) {
10076
+ await this.deleteQuietly(name);
10077
+ throw new Error(`openshell could not create the sandbox: ${describeExecError(e)}`);
10078
+ }
10079
+ o.emit.emit(`sandbox_created: ${name}`, { role: plan.role, data: { event: "sandbox_created", sandbox: name, ms: Date.now() - t0 } });
10080
+ const handle = { runner: "sandbox", sandbox: name, sandboxBin: cfg.bin };
10081
+ try {
10082
+ const branch = `crew-run-${name.slice(-8)}`;
10083
+ const clone = [
10084
+ `git clone --quiet --shared ${shQuote2(`${cfg.mirrorDir}/${repoName}.git`)} ${shQuote2(cfg.workDir)}`,
10085
+ `cd ${shQuote2(cfg.workDir)}`,
10086
+ `git remote set-url origin ${shQuote2(origin)}`,
10087
+ `git checkout --quiet -b ${shQuote2(branch)}`
10088
+ ].join(" && ");
10089
+ await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
10090
+ const harness = cfg.agentBin ?? basename7(plan.bin);
10091
+ const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
10092
+ if (viaUpload) {
10093
+ const tmp = mkdtempSync(join23(tmpdir3(), "crew-prompt-"));
10094
+ try {
10095
+ const file = join23(tmp, "prompt.txt");
10096
+ writeFileSync14(file, plan.prompt, { mode: 384 });
10097
+ await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
10098
+ } finally {
10099
+ rmSync13(tmp, { recursive: true, force: true });
10100
+ }
10101
+ }
10102
+ const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
10103
+ const child = spawn3(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
10104
+ env: process.env,
10105
+ stdio: ["pipe", o.streamFd, "inherit"],
10106
+ detached: true
10107
+ });
10108
+ return { child, handle: { ...handle, pid: child.pid }, ...viaUpload ? { promptDelivered: true } : {} };
10109
+ } catch (e) {
10110
+ await this.release(handle, o.emit);
10111
+ throw new Error(`could not prepare the sandbox ${name}: ${describeExecError(e)}`);
10112
+ }
10113
+ }
10114
+ async kill(handle) {
10115
+ if (handle.sandbox) await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
10116
+ if (handle.pid !== void 0) await killRunGroup(handle.pid, { graceMs: 2e3 });
10117
+ }
10118
+ isAlive(handle) {
10119
+ return new HostRunner().isAlive(handle);
10120
+ }
10121
+ reattach(meta) {
10122
+ return { runner: "sandbox", pid: meta.pid, sandbox: meta.sandbox, sandboxBin: meta.sandboxBin };
10123
+ }
10124
+ async release(handle, emit) {
10125
+ if (!handle.sandbox) return;
10126
+ const t0 = Date.now();
10127
+ await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
10128
+ emit.emit(`sandbox_deleted: ${handle.sandbox}`, { data: { event: "sandbox_deleted", sandbox: handle.sandbox, ms: Date.now() - t0 } });
10129
+ }
10130
+ async deleteQuietly(name, bin = this.cfg.bin) {
10131
+ try {
10132
+ await execFileP(bin, ["sandbox", "delete", name], { timeout: 6e4 });
10133
+ } catch {
10134
+ }
10135
+ }
10136
+ };
10137
+ function describeExecError(e) {
10138
+ const err3 = e;
10139
+ return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
10140
+ }
10141
+ function gitOrigin(cwd) {
10142
+ try {
10143
+ return execFileSync6("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
10144
+ } catch {
10145
+ return null;
10146
+ }
10147
+ }
10148
+ function runnerFor(kind, sandbox) {
10149
+ return kind === "sandbox" ? new SandboxRunner(sandbox) : new HostRunner();
10150
+ }
10151
+ function runnerOfSidecar(sc) {
10152
+ return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
10153
+ }
10154
+ function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync6(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
10155
+ const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
10156
+ let version;
10157
+ try {
10158
+ version = run3(bin, ["--version"]).trim();
10159
+ } catch (e) {
10160
+ return { ok: false, detail: `${bin} is not installed or not on the scheduler PATH (${e.message.split("\n")[0]})` };
10161
+ }
10162
+ try {
10163
+ run3(bin, ["status"]);
10164
+ } catch (e) {
10165
+ const err3 = e;
10166
+ return {
10167
+ ok: false,
10168
+ detail: `${version}: the gateway does not answer (${(err3.stderr || err3.message).trim().split("\n").filter(Boolean).slice(0, 2).join(" / ")}). Start it; on Docker Desktop its docker section needs supervisor_network_mode = "bridge", and allow_driver_config = true for the cache volume mount.`
10169
+ };
10170
+ }
10171
+ return { ok: true, detail: version };
10172
+ }
10173
+
9509
10174
  // src/agent.ts
9510
10175
  var DIGEST_MAX_AGE_SECONDS = 600;
9511
10176
  var LEGACY_TOOLCHAIN_HARNESS = "claude";
@@ -9533,7 +10198,7 @@ var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
9533
10198
  function streamShowsAllowanceRefusal(streamPath) {
9534
10199
  let raw;
9535
10200
  try {
9536
- raw = readFileSync19(streamPath, "utf8");
10201
+ raw = readFileSync20(streamPath, "utf8");
9537
10202
  } catch {
9538
10203
  return false;
9539
10204
  }
@@ -9556,13 +10221,13 @@ function streamShowsAllowanceRefusal(streamPath) {
9556
10221
  }
9557
10222
  return false;
9558
10223
  }
9559
- var digestPathFor = (stateDir, routeLabel, role, runId) => join22(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
10224
+ var digestPathFor = (stateDir, routeLabel, role, runId) => join24(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9560
10225
  function planAgentRun(o) {
9561
- const briefPath = join22(o.route.promptsDir, "personas", `lane-${o.role}.md`);
10226
+ const briefPath = join24(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9562
10227
  if (!existsSync21(briefPath)) {
9563
10228
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
9564
10229
  }
9565
- const commonPath = join22(o.route.promptsDir, "personas", "common.md");
10230
+ const commonPath = join24(o.route.promptsDir, "personas", "common.md");
9566
10231
  const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync21(commonPath);
9567
10232
  const now = o.now ?? (() => Date.now());
9568
10233
  const routeLabel = routeSlug(o.route.route);
@@ -9572,10 +10237,10 @@ function planAgentRun(o) {
9572
10237
  let ageSeconds;
9573
10238
  if (existsSync21(digestPath2)) {
9574
10239
  ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
9575
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync19(digestPath2, "utf8");
10240
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync20(digestPath2, "utf8");
9576
10241
  }
9577
- const common = usesCommon ? readFileSync19(commonPath, "utf8") : "";
9578
- const brief = readFileSync19(briefPath, "utf8");
10242
+ const common = usesCommon ? readFileSync20(commonPath, "utf8") : "";
10243
+ const brief = readFileSync20(briefPath, "utf8");
9579
10244
  const effectiveCommon = o.divergedPrompt ? "" : common;
9580
10245
  const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
9581
10246
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -9596,8 +10261,8 @@ function planAgentRun(o) {
9596
10261
  brief: expanded.text,
9597
10262
  digest
9598
10263
  });
9599
- const promptSha = createHash4("sha256").update(expanded.text).digest("hex");
9600
- const streamsDir = join22(o.stateDir, "streams");
10264
+ const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
10265
+ const streamsDir = join24(o.stateDir, "streams");
9601
10266
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
9602
10267
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
9603
10268
  const { model } = seat;
@@ -9658,6 +10323,7 @@ function planAgentRun(o) {
9658
10323
  workspaceAuth: toolchain.auth === "workspace",
9659
10324
  provider: toolchainProvider(toolchain),
9660
10325
  maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
10326
+ ...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
9661
10327
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
9662
10328
  warnings: [...seat.warnings ?? [], ...expanded.warnings],
9663
10329
  prompt,
@@ -9682,8 +10348,8 @@ function planAgentRun(o) {
9682
10348
  digestAttached: digest !== void 0,
9683
10349
  digestAgeSeconds: ageSeconds,
9684
10350
  model,
9685
- streamPath: join22(streamsDir, `${base}.jsonl`),
9686
- eventsPath: join22(streamsDir, `${base}.events.jsonl`),
10351
+ streamPath: join24(streamsDir, `${base}.jsonl`),
10352
+ eventsPath: join24(streamsDir, `${base}.events.jsonl`),
9687
10353
  ticket: o.ticket,
9688
10354
  ticketRecordId: o.ticketRecordId ?? null,
9689
10355
  repo: o.repo ?? null,
@@ -9714,7 +10380,7 @@ function describePlan(p) {
9714
10380
  var pidPathFor = (streamPath) => `${streamPath}.pid`;
9715
10381
  function readRunSidecar(streamPath) {
9716
10382
  try {
9717
- const sc = JSON.parse(readFileSync19(pidPathFor(streamPath), "utf8"));
10383
+ const sc = JSON.parse(readFileSync20(pidPathFor(streamPath), "utf8"));
9718
10384
  return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
9719
10385
  } catch {
9720
10386
  return null;
@@ -9747,7 +10413,7 @@ function identityOf(plan, adapterHarness) {
9747
10413
  promptSha: plan.promptSha
9748
10414
  };
9749
10415
  }
9750
- async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes) {
10416
+ async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
9751
10417
  const elapsedMs = Date.now() - Date.parse(spawnedAt);
9752
10418
  const limit = limitMinutes ?? Math.round(elapsedMs / 6e4);
9753
10419
  emit.emit(`run_killed: ${role} run exceeded its wall-clock limit of ${limit} min (ran ${Math.round(elapsedMs / 6e4)} min)`, {
@@ -9755,13 +10421,13 @@ async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes)
9755
10421
  ...ticket ? { ticket } : {},
9756
10422
  data: { event: "run_killed", reason: WALL_CLOCK_REASON, limitMinutes: limit, elapsedMs, pid }
9757
10423
  });
9758
- await killRunGroup(pid);
10424
+ await kill();
9759
10425
  return { limitMinutes: limit, elapsedMs };
9760
10426
  }
9761
10427
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
9762
10428
  function openSink(path) {
9763
10429
  try {
9764
- mkdirSync12(dirname8(path), { recursive: true });
10430
+ mkdirSync13(dirname8(path), { recursive: true });
9765
10431
  return createWriteStream(path, { flags: "a" });
9766
10432
  } catch {
9767
10433
  return void 0;
@@ -9943,7 +10609,7 @@ var RunObserver = class {
9943
10609
  };
9944
10610
  function writeRunMeta(id, startedAt, liveTicket, extra) {
9945
10611
  try {
9946
- writeFileSync13(`${id.streamPath}.meta.json`, JSON.stringify({
10612
+ writeFileSync15(`${id.streamPath}.meta.json`, JSON.stringify({
9947
10613
  route: id.route,
9948
10614
  role: id.role,
9949
10615
  hint: id.ticket,
@@ -10050,7 +10716,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10050
10716
  if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
10051
10717
  if (sent.counterRetry) {
10052
10718
  try {
10053
- queueTicketCounters(stateDirOfStream(id.streamPath), basename7(id.streamPath, ".jsonl"), {
10719
+ queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
10054
10720
  version: 1,
10055
10721
  route: id.route,
10056
10722
  role: id.role,
@@ -10065,7 +10731,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10065
10731
  } catch (e) {
10066
10732
  if (e instanceof AgentLogUnreachableError) {
10067
10733
  try {
10068
- queueAgentRun(stateDirOfStream(id.streamPath), basename7(id.streamPath, ".jsonl"), {
10734
+ queueAgentRun(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
10069
10735
  version: 1,
10070
10736
  route: id.route,
10071
10737
  role: id.role,
@@ -10095,102 +10761,125 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10095
10761
  }
10096
10762
  function spawnAgent(plan, emit, hintMoved) {
10097
10763
  const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
10764
+ const runner = runnerFor(plan.runner, plan.sandboxConfig);
10098
10765
  const started = Date.now();
10099
10766
  const startedAt = new Date(started).toISOString();
10100
10767
  for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
10101
10768
  const eventsSink = openSink(plan.eventsPath);
10102
10769
  const id = identityOf(plan, adapter.harness);
10103
- return new Promise((resolve7, reject) => {
10104
- let streamFd;
10105
- try {
10106
- mkdirSync12(dirname8(plan.streamPath), { recursive: true });
10107
- streamFd = openSync3(plan.streamPath, "a");
10108
- } catch (e) {
10109
- void endSink(eventsSink);
10110
- reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
10111
- return;
10112
- }
10113
- const child = spawn2(plan.bin, plan.args, {
10114
- cwd: plan.cwd,
10115
- env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv },
10116
- stdio: ["pipe", streamFd, "inherit"],
10117
- detached: true
10118
- });
10770
+ let streamFd;
10771
+ try {
10772
+ mkdirSync13(dirname8(plan.streamPath), { recursive: true });
10773
+ streamFd = openSync3(plan.streamPath, "a");
10774
+ } catch (e) {
10775
+ void endSink(eventsSink);
10776
+ return Promise.reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
10777
+ }
10778
+ const failed = (e) => {
10779
+ void endSink(eventsSink);
10780
+ return Promise.reject(new AgentSpawnError(`cannot run ${plan.bin}: ${e.message}`));
10781
+ };
10782
+ let begun;
10783
+ try {
10784
+ begun = runner.start(plan, { env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv }, streamFd, emit });
10785
+ } catch (e) {
10119
10786
  closeSync3(streamFd);
10120
- const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
10121
- const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
10122
- if (child.pid !== void 0) {
10123
- const sidecar = {
10124
- pid: child.pid,
10125
- ownerPid: process.pid,
10126
- spawnedAt,
10127
- role: plan.role,
10128
- route: plan.route,
10129
- ...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
10130
- ticket: plan.ticket ?? null,
10131
- ticketRecordId: plan.ticketRecordId ?? null,
10132
- repo: plan.repo ?? null,
10133
- routeName: plan.routeName,
10134
- model: plan.model,
10135
- toolchain: id.toolchain,
10136
- harness: id.harness,
10137
- provider: id.provider,
10138
- eventsPath: plan.eventsPath,
10139
- agentLog: id.agentLog,
10140
- promptVersion: plan.promptVersion,
10141
- promptSha: plan.promptSha
10142
- };
10143
- try {
10144
- writeFileSync13(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
10145
- } catch {
10787
+ return failed(e);
10788
+ }
10789
+ if (begun instanceof Promise) {
10790
+ return begun.then(
10791
+ (run3) => {
10792
+ closeSync3(streamFd);
10793
+ return supervise(run3);
10794
+ },
10795
+ (e) => {
10796
+ closeSync3(streamFd);
10797
+ return failed(e);
10146
10798
  }
10147
- }
10148
- const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
10149
- const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
10150
- obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
10151
- if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
10152
- const tail3 = new FileTail(plan.streamPath);
10153
- const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
10154
- let killed;
10155
- const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
10156
- const pid = child.pid;
10157
- void killForDeadline(pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
10158
- killed = k;
10159
- });
10160
- }, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
10161
- let spawnFailed = false;
10162
- child.on("error", (err3) => {
10163
- spawnFailed = true;
10164
- clearInterval(timer);
10165
- clearTimeout(limitTimer);
10166
- reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
10167
- });
10168
- child.on("close", async (code) => {
10169
- clearInterval(timer);
10170
- clearTimeout(limitTimer);
10171
- obs.feed(tail3.poll());
10172
- obs.end();
10173
- await endSink(eventsSink);
10174
- if (spawnFailed) return;
10175
- if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
10176
- killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
10799
+ );
10800
+ }
10801
+ closeSync3(streamFd);
10802
+ return supervise(begun);
10803
+ function supervise({ child, handle, promptDelivered }) {
10804
+ return new Promise((resolve7, reject) => {
10805
+ const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
10806
+ const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
10807
+ if (child.pid !== void 0) {
10808
+ const sidecar = {
10809
+ pid: child.pid,
10810
+ ownerPid: process.pid,
10811
+ spawnedAt,
10812
+ role: plan.role,
10813
+ route: plan.route,
10814
+ ...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
10815
+ ...handle.runner === "sandbox" ? { runner: "sandbox", sandbox: handle.sandbox, sandboxBin: handle.sandboxBin } : {},
10816
+ ticket: plan.ticket ?? null,
10817
+ ticketRecordId: plan.ticketRecordId ?? null,
10818
+ repo: plan.repo ?? null,
10819
+ routeName: plan.routeName,
10820
+ model: plan.model,
10821
+ toolchain: id.toolchain,
10822
+ harness: id.harness,
10823
+ provider: id.provider,
10824
+ eventsPath: plan.eventsPath,
10825
+ agentLog: id.agentLog,
10826
+ promptVersion: plan.promptVersion,
10827
+ promptSha: plan.promptSha
10828
+ };
10829
+ try {
10830
+ writeFileSync15(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
10831
+ } catch {
10832
+ }
10177
10833
  }
10178
- const { ms: ms3, handoffPending } = await finishRun(
10179
- id,
10180
- obs,
10181
- emit,
10182
- startedAt,
10183
- { code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
10184
- plan.agentLog?.apiKey,
10185
- hintMoved
10186
- );
10187
- resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
10188
- });
10189
- child.stdin?.on("error", (err3) => {
10190
- emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
10834
+ const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
10835
+ const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
10836
+ obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
10837
+ if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
10838
+ const tail3 = new FileTail(plan.streamPath);
10839
+ const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
10840
+ let killed;
10841
+ const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
10842
+ void killForDeadline(() => runner.kill(handle, WALL_CLOCK_REASON), child.pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
10843
+ killed = k;
10844
+ });
10845
+ }, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
10846
+ let spawnFailed = false;
10847
+ child.on("error", (err3) => {
10848
+ spawnFailed = true;
10849
+ clearInterval(timer);
10850
+ clearTimeout(limitTimer);
10851
+ void runner.release(handle, emit);
10852
+ reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
10853
+ });
10854
+ child.on("close", async (code) => {
10855
+ clearInterval(timer);
10856
+ clearTimeout(limitTimer);
10857
+ obs.feed(tail3.poll());
10858
+ obs.end();
10859
+ await endSink(eventsSink);
10860
+ if (spawnFailed) return;
10861
+ await runner.release(handle, emit);
10862
+ if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
10863
+ killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
10864
+ }
10865
+ const { ms: ms3, handoffPending } = await finishRun(
10866
+ id,
10867
+ obs,
10868
+ emit,
10869
+ startedAt,
10870
+ { code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
10871
+ plan.agentLog?.apiKey,
10872
+ hintMoved
10873
+ );
10874
+ resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
10875
+ });
10876
+ child.stdin?.on("error", (err3) => {
10877
+ emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
10878
+ });
10879
+ if (promptDelivered) child.stdin?.end();
10880
+ else child.stdin?.end(plan.prompt);
10191
10881
  });
10192
- child.stdin?.end(plan.prompt);
10193
- });
10882
+ }
10194
10883
  }
10195
10884
  async function reattachRun(streamPath, sc, emit, o = {}) {
10196
10885
  const adapter = adapterFor(sc.harness);
@@ -10202,6 +10891,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
10202
10891
  obs.muted = true;
10203
10892
  obs.feed(tail3.poll());
10204
10893
  obs.muted = false;
10894
+ const runner = runnerOfSidecar(sc);
10205
10895
  const adopted = isAlive(sc);
10206
10896
  let killed;
10207
10897
  if (adopted) {
@@ -10210,7 +10900,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
10210
10900
  while (isAlive(sc)) {
10211
10901
  obs.feed(tail3.poll());
10212
10902
  if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
10213
- killed = await killForDeadline(sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
10903
+ killed = await killForDeadline(() => runner.kill(runner.reattach(sc), WALL_CLOCK_REASON), sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
10214
10904
  continue;
10215
10905
  }
10216
10906
  await new Promise((r) => setTimeout(r, pollMs));
@@ -10219,6 +10909,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
10219
10909
  obs.feed(tail3.poll());
10220
10910
  obs.end();
10221
10911
  await endSink(eventsSink);
10912
+ await runner.release(runner.reattach(sc), emit);
10222
10913
  const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
10223
10914
  const { ms: ms3, ticket } = await finishRun(
10224
10915
  id,
@@ -10349,9 +11040,9 @@ async function mintMissingSeatKeys(route2, o) {
10349
11040
  out.push({ role, action: "no_agent" });
10350
11041
  continue;
10351
11042
  }
10352
- const slug = slugFor(route2, agentId);
11043
+ const slug2 = slugFor(route2, agentId);
10353
11044
  try {
10354
- if ((await store.get(host, slug))?.apiKey) {
11045
+ if ((await store.get(host, slug2))?.apiKey) {
10355
11046
  out.push({ role, action: "already_stored" });
10356
11047
  continue;
10357
11048
  }
@@ -10389,7 +11080,7 @@ async function mintMissingSeatKeys(route2, o) {
10389
11080
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
10390
11081
  ...body.id ? { keyId: body.id } : {}
10391
11082
  };
10392
- await store.set(host, slug, session);
11083
+ await store.set(host, slug2, session);
10393
11084
  } catch (e) {
10394
11085
  let revoked = false;
10395
11086
  if (body.id) {
@@ -10453,9 +11144,9 @@ async function revokeSeatKeys(route2, o) {
10453
11144
  out.push({ role, action: "no_agent" });
10454
11145
  continue;
10455
11146
  }
10456
- const slug = slugFor(route2, agentId);
11147
+ const slug2 = slugFor(route2, agentId);
10457
11148
  try {
10458
- const session = await store.get(host, slug);
11149
+ const session = await store.get(host, slug2);
10459
11150
  if (!session?.apiKey) {
10460
11151
  out.push({ role, action: "not_stored" });
10461
11152
  continue;
@@ -10476,7 +11167,7 @@ async function revokeSeatKeys(route2, o) {
10476
11167
  out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
10477
11168
  continue;
10478
11169
  }
10479
- await store.clear(host, slug);
11170
+ await store.clear(host, slug2);
10480
11171
  out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
10481
11172
  } catch (e) {
10482
11173
  out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
@@ -10528,7 +11219,7 @@ var realDeps = {
10528
11219
  resolve7(r);
10529
11220
  }
10530
11221
  };
10531
- const child = spawn3(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 });
10532
11223
  const killGroup = () => {
10533
11224
  try {
10534
11225
  if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
@@ -10626,17 +11317,17 @@ var REMEDY = {
10626
11317
  timeout: "the harness did not answer in time \u2014 run it by hand to see where it stalls",
10627
11318
  "wrong-answer": "the agent answered but did not name the workspace \u2014 run it by hand to see what it said"
10628
11319
  };
10629
- function classify(spec, run2, seen) {
11320
+ function classify(spec, run3, seen) {
10630
11321
  const bin = spec.launcher ?? spec.bin;
10631
11322
  const fail = (failure, what) => ({ failure, detail: `${what} \u2014 ${REMEDY[failure]}` });
10632
- if (run2.spawnError) {
10633
- if (run2.spawnError.code === "ENOENT") return fail("binary-not-found", `\`${bin}\` is not on the scheduler PATH`);
10634
- 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})`);
10635
11326
  }
10636
- 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");
10637
11328
  const text2 = `${seen.toolOutput}
10638
- ${run2.stderr}`;
10639
- 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)) {
10640
11331
  return fail("key-refused", "the tracker returned 401/403 to the MCP call");
10641
11332
  }
10642
11333
  if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text2}
@@ -10644,8 +11335,8 @@ ${seen.texts.join("\n")}`)) {
10644
11335
  return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
10645
11336
  }
10646
11337
  const answered = seen.texts.length > 0 || seen.finalText;
10647
- if (!answered || LOGIN.test(run2.stderr) || LOGIN.test(seen.finalText ?? "")) {
10648
- 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`);
10649
11340
  }
10650
11341
  return fail("wrong-answer", `the agent replied "${(seen.finalText ?? seen.texts.at(-1) ?? "").trim().slice(0, 80)}"`);
10651
11342
  }
@@ -10673,9 +11364,9 @@ async function runSelftest(i, deps = realDeps) {
10673
11364
  ...adapter.runEnv(spec),
10674
11365
  [KEY_VAR]: i.apiKey
10675
11366
  };
10676
- const cwd = mkdtempSync(join23(tmpdir2(), "crew-selftest-"));
11367
+ const cwd = mkdtempSync2(join25(tmpdir4(), "crew-selftest-"));
10677
11368
  try {
10678
- const run2 = await deps.run({
11369
+ const run3 = await deps.run({
10679
11370
  bin: invocation.bin,
10680
11371
  args: invocation.args,
10681
11372
  cwd,
@@ -10684,18 +11375,18 @@ async function runSelftest(i, deps = realDeps) {
10684
11375
  `,
10685
11376
  timeoutMs: i.timeoutMs ?? SELFTEST_TIMEOUT_MS
10686
11377
  });
10687
- const seen = observe(spec, run2.stdout);
11378
+ const seen = observe(spec, run3.stdout);
10688
11379
  const answer = [seen.finalText ?? "", ...seen.texts].join("\n");
10689
11380
  const usage2 = { ...seen.tokens !== void 0 ? { tokens: seen.tokens } : {}, ...seen.costUsd !== void 0 ? { costUsd: seen.costUsd } : {} };
10690
- if (!run2.spawnError && !run2.timedOut && answer.toLowerCase().includes(i.workspaceName.toLowerCase())) {
11381
+ if (!run3.spawnError && !run3.timedOut && answer.toLowerCase().includes(i.workspaceName.toLowerCase())) {
10691
11382
  const auth = spec.harness === "claude" ? ` on ${describeClaudeAuth(spec)}` : "";
10692
11383
  return finish({ ok: true, workspace: i.workspaceName, detail: `read workspace "${i.workspaceName}"${auth}`, ...usage2 });
10693
11384
  }
10694
- const { failure, detail } = classify(spec, run2, seen);
11385
+ const { failure, detail } = classify(spec, run3, seen);
10695
11386
  return finish({ ok: false, failure, detail, ...usage2 });
10696
11387
  } finally {
10697
11388
  try {
10698
- rmSync12(cwd, { recursive: true, force: true });
11389
+ rmSync14(cwd, { recursive: true, force: true });
10699
11390
  } catch {
10700
11391
  }
10701
11392
  }
@@ -10706,11 +11397,11 @@ function formatResult(r) {
10706
11397
  const secs = `${(r.ms / 1e3).toFixed(1)}s`;
10707
11398
  return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
10708
11399
  }
10709
- var fileFor2 = (stateDir) => join23(stateDir, "selftest.json");
11400
+ var fileFor2 = (stateDir) => join25(stateDir, "selftest.json");
10710
11401
  function readSelftest(stateDir) {
10711
11402
  try {
10712
11403
  const p = fileFor2(stateDir);
10713
- return existsSync22(p) ? JSON.parse(readFileSync20(p, "utf8")) : {};
11404
+ return existsSync22(p) ? JSON.parse(readFileSync21(p, "utf8")) : {};
10714
11405
  } catch {
10715
11406
  return {};
10716
11407
  }
@@ -10718,8 +11409,8 @@ function readSelftest(stateDir) {
10718
11409
  function recordSelftest(stateDir, results) {
10719
11410
  const all = readSelftest(stateDir);
10720
11411
  for (const r of results) all[r.toolchain] = r;
10721
- mkdirSync13(stateDir, { recursive: true });
10722
- writeFileSync14(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
11412
+ mkdirSync14(stateDir, { recursive: true });
11413
+ writeFileSync16(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10723
11414
  `);
10724
11415
  }
10725
11416
  function doctorSelftestLines(stored, toolchains) {
@@ -10731,7 +11422,7 @@ function doctorSelftestLines(stored, toolchains) {
10731
11422
  }
10732
11423
 
10733
11424
  // src/cli.ts
10734
- import { resolve as resolve6, dirname as dirname25, join as join47, basename as basename14 } from "path";
11425
+ import { resolve as resolve6, dirname as dirname25, join as join49, basename as basename15 } from "path";
10735
11426
  import { fileURLToPath } from "url";
10736
11427
  import { hostname as hostname4, homedir as homedir11 } from "os";
10737
11428
  import { spawnSync as spawnSync10 } from "child_process";
@@ -10928,14 +11619,14 @@ function rosterMarkdown(roster, meId) {
10928
11619
  }
10929
11620
 
10930
11621
  // src/connect-resume.ts
10931
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync13, writeFileSync as writeFileSync15 } from "fs";
10932
- import { dirname as dirname9, join as join24 } 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";
10933
11624
  function lastSessionFile(stateDir) {
10934
- return join24(stateDir, "connect-session.json");
11625
+ return join26(stateDir, "connect-session.json");
10935
11626
  }
10936
11627
  function readPointers(stateDir) {
10937
11628
  try {
10938
- const parsed = JSON.parse(readFileSync21(lastSessionFile(stateDir), "utf8"));
11629
+ const parsed = JSON.parse(readFileSync22(lastSessionFile(stateDir), "utf8"));
10939
11630
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
10940
11631
  const out = {};
10941
11632
  for (const [host, v] of Object.entries(parsed)) {
@@ -10949,8 +11640,8 @@ function readPointers(stateDir) {
10949
11640
  }
10950
11641
  function writePointers(stateDir, pointers) {
10951
11642
  const file = lastSessionFile(stateDir);
10952
- mkdirSync14(dirname9(file), { recursive: true });
10953
- writeFileSync15(file, `${JSON.stringify(pointers, null, 2)}
11643
+ mkdirSync15(dirname9(file), { recursive: true });
11644
+ writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
10954
11645
  `, { mode: 384 });
10955
11646
  }
10956
11647
  function rememberSession(stateDir, host, workspaceSlug) {
@@ -10967,7 +11658,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
10967
11658
  const { [host]: _old, ...rest } = all;
10968
11659
  const next = left.length ? { ...rest, [host]: left } : rest;
10969
11660
  if (Object.keys(next).length) writePointers(stateDir, next);
10970
- else rmSync13(lastSessionFile(stateDir), { force: true });
11661
+ else rmSync15(lastSessionFile(stateDir), { force: true });
10971
11662
  } catch {
10972
11663
  }
10973
11664
  }
@@ -11007,7 +11698,7 @@ function forgetSession(stateDir, host) {
11007
11698
  try {
11008
11699
  const { [host]: _gone, ...rest } = readPointers(stateDir);
11009
11700
  if (Object.keys(rest).length) writePointers(stateDir, rest);
11010
- else rmSync13(lastSessionFile(stateDir), { force: true });
11701
+ else rmSync15(lastSessionFile(stateDir), { force: true });
11011
11702
  } catch {
11012
11703
  }
11013
11704
  }
@@ -11035,15 +11726,15 @@ async function signOutSessions(o) {
11035
11726
  }
11036
11727
 
11037
11728
  // src/reset.ts
11038
- import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync9, rmSync as rmSync14, statSync as statSync8, writeFileSync as writeFileSync16 } from "fs";
11039
- import { dirname as dirname10, join as join25 } 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";
11040
11731
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
11041
11732
  function clonedCheckoutsFile(stateDir) {
11042
- return join25(stateDir, "cloned-checkouts.json");
11733
+ return join27(stateDir, "cloned-checkouts.json");
11043
11734
  }
11044
11735
  function readClonedCheckouts(stateDir) {
11045
11736
  try {
11046
- const parsed = JSON.parse(readFileSync22(clonedCheckoutsFile(stateDir), "utf8"));
11737
+ const parsed = JSON.parse(readFileSync23(clonedCheckoutsFile(stateDir), "utf8"));
11047
11738
  return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
11048
11739
  } catch {
11049
11740
  return [];
@@ -11053,8 +11744,8 @@ function recordClonedCheckout(stateDir, dir) {
11053
11744
  const known = readClonedCheckouts(stateDir);
11054
11745
  if (known.includes(dir)) return;
11055
11746
  const file = clonedCheckoutsFile(stateDir);
11056
- mkdirSync15(dirname10(file), { recursive: true });
11057
- writeFileSync16(file, `${JSON.stringify([...known, dir], null, 2)}
11747
+ mkdirSync16(dirname10(file), { recursive: true });
11748
+ writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
11058
11749
  `);
11059
11750
  }
11060
11751
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -11109,7 +11800,7 @@ async function runReset(o, fx) {
11109
11800
  if (o.beforeState) steps.push(...await o.beforeState());
11110
11801
  steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
11111
11802
  if (existsSync23(o.configFile)) {
11112
- if (!o.dryRun) rmSync14(o.configFile, { force: true });
11803
+ if (!o.dryRun) rmSync16(o.configFile, { force: true });
11113
11804
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
11114
11805
  } else {
11115
11806
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -11125,21 +11816,21 @@ function removeCheckout(dir, dryRun, fx) {
11125
11816
  if (linkedWorktrees.length) {
11126
11817
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
11127
11818
  }
11128
- if (!dryRun) rmSync14(dir, { recursive: true, force: true });
11819
+ if (!dryRun) rmSync16(dir, { recursive: true, force: true });
11129
11820
  return { step: "checkout", target: dir, action: mark(dryRun) };
11130
11821
  }
11131
11822
  function removeState(stateDir, dryRun) {
11132
11823
  const out = [];
11133
- const targets = [join25(stateDir, "resolved"), join25(stateDir, "keys"), clonedCheckoutsFile(stateDir), join25(stateDir, "connect-session.json"), join25(stateDir, "reload-requested"), join25(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")];
11134
11825
  try {
11135
11826
  for (const name of readdirSync9(stateDir)) {
11136
- if (name.startsWith(".") && statSync8(join25(stateDir, name)).isFile()) targets.push(join25(stateDir, name));
11827
+ if (name.startsWith(".") && statSync8(join27(stateDir, name)).isFile()) targets.push(join27(stateDir, name));
11137
11828
  }
11138
11829
  } catch {
11139
11830
  }
11140
11831
  for (const t of targets) {
11141
11832
  if (!existsSync23(t)) continue;
11142
- if (!dryRun) rmSync14(t, { recursive: true, force: true });
11833
+ if (!dryRun) rmSync16(t, { recursive: true, force: true });
11143
11834
  out.push({ step: "state", target: t, action: mark(dryRun) });
11144
11835
  }
11145
11836
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -11153,7 +11844,7 @@ function wipeStateDir(stateDir, dryRun) {
11153
11844
  return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
11154
11845
  }
11155
11846
  if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
11156
- if (!dryRun) for (const n of names) rmSync14(join25(stateDir, n), { recursive: true, force: true });
11847
+ if (!dryRun) for (const n of names) rmSync16(join27(stateDir, n), { recursive: true, force: true });
11157
11848
  return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
11158
11849
  }
11159
11850
  function describeResetStep(s) {
@@ -11177,21 +11868,21 @@ async function runDeprovision(o, fx, deps) {
11177
11868
  };
11178
11869
  const afterScheduler = async () => {
11179
11870
  const steps2 = [];
11180
- for (const run2 of deps.liveRuns()) {
11181
- 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})`;
11182
11873
  if (o.dryRun) {
11183
11874
  steps2.push({ step: "run", target, action: "would-remove" });
11184
11875
  continue;
11185
11876
  }
11186
11877
  try {
11187
- await deps.killRun(run2);
11188
- deps.recordKill(run2);
11878
+ await deps.killRun(run3);
11879
+ deps.recordKill(run3);
11189
11880
  } catch (e) {
11190
11881
  steps2.push({ step: "run", target, action: "failed", detail: e.message });
11191
11882
  continue;
11192
11883
  }
11193
11884
  try {
11194
- await deps.handBack(run2);
11885
+ await deps.handBack(run3);
11195
11886
  steps2.push({ step: "run", target, action: "removed", detail: "killed" });
11196
11887
  } catch (e) {
11197
11888
  steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
@@ -11238,8 +11929,8 @@ import { spawnSync as spawnSync6 } from "child_process";
11238
11929
  import { existsSync as existsSync26, readdirSync as readdirSync12 } from "fs";
11239
11930
 
11240
11931
  // src/interrupted.ts
11241
- import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
11242
- import { join as join26 } from "path";
11932
+ import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
11933
+ import { join as join28 } from "path";
11243
11934
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
11244
11935
  function interruptedComment(b) {
11245
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.";
@@ -11260,19 +11951,19 @@ function runFinished(streamPath) {
11260
11951
  const path = `${streamPath}.meta.json`;
11261
11952
  if (!existsSync24(path)) return false;
11262
11953
  try {
11263
- return JSON.parse(readFileSync23(path, "utf8")).running !== true;
11954
+ return JSON.parse(readFileSync24(path, "utf8")).running !== true;
11264
11955
  } catch {
11265
11956
  return true;
11266
11957
  }
11267
11958
  }
11268
11959
  function orphanedStreams(o) {
11269
- const dir = join26(o.stateDir, "streams");
11960
+ const dir = join28(o.stateDir, "streams");
11270
11961
  if (!existsSync24(dir)) return [];
11271
11962
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
11272
11963
  const out = [];
11273
11964
  for (const name of readdirSync10(dir)) {
11274
11965
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
11275
- const streamPath = join26(dir, name);
11966
+ const streamPath = join28(dir, name);
11276
11967
  if (runFinished(streamPath)) continue;
11277
11968
  const sidecar = readRunSidecar(streamPath);
11278
11969
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -11319,283 +12010,79 @@ async function reconcileRuns(o) {
11319
12010
  }
11320
12011
  } else {
11321
12012
  await work;
11322
- }
11323
- }
11324
- return { adopted, interrupted };
11325
- }
11326
-
11327
- // src/live-runs.ts
11328
- import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
11329
- import { join as join27 } from "path";
11330
- function liveRuns(stateDir, isAlive = runIsAlive) {
11331
- const dir = join27(stateDir, "streams");
11332
- if (!existsSync25(dir)) return [];
11333
- const out = [];
11334
- for (const name of readdirSync11(dir)) {
11335
- if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
11336
- const streamPath = join27(dir, name);
11337
- if (runFinished(streamPath)) continue;
11338
- const sidecar = readRunSidecar(streamPath);
11339
- if (!sidecar || !isAlive(sidecar)) continue;
11340
- out.push({ ...sidecar, streamPath });
11341
- }
11342
- return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
11343
- }
11344
- function siblingRuns(runs, ownTicketRecordId) {
11345
- return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
11346
- }
11347
- async function settleRunEnd(o) {
11348
- const { tracker, emit } = o;
11349
- const all = o.runs ?? liveRuns(o.stateDir);
11350
- const slug = routeSlug(o.route.route);
11351
- const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug), o.ownTicketRecordId);
11352
- const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
11353
- const newest = (rs) => rs[rs.length - 1];
11354
- if (o.memberId) {
11355
- try {
11356
- if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
11357
- else await tracker.setCrewStatus(o.memberId, "idle");
11358
- } catch (e) {
11359
- emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
11360
- }
11361
- }
11362
- try {
11363
- if (shipSiblings.length) {
11364
- const n = newest(shipSiblings);
11365
- await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
11366
- } else {
11367
- await tracker.beatIdle(o.shipName);
11368
- }
11369
- } catch (e) {
11370
- emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
11371
- }
11372
- }
11373
- function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
11374
- try {
11375
- return worktreeForNumber(d, n)?.path ?? null;
11376
- } catch {
11377
- return null;
11378
- }
11379
- }) {
11380
- const ownRoute = run2.route === routeSlug(route2.route);
11381
- const repoDir = run2.repo ? reposOf(route2).find((t) => t.name === run2.repo)?.dir : void 0;
11382
- const num = run2.ticket?.replace(/^\D+/, "") ?? "";
11383
- return {
11384
- role: run2.role,
11385
- seat: ownRoute ? seatIds[run2.role] ?? null : null,
11386
- ticket: run2.ticket,
11387
- ticketRecordId: run2.ticketRecordId,
11388
- repo: run2.repo ?? null,
11389
- worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
11390
- stream: run2.streamPath,
11391
- pid: run2.pid,
11392
- startedAt: run2.spawnedAt,
11393
- deadline: run2.deadline ?? null
11394
- };
11395
- }
11396
-
11397
- // src/passenger-containers.ts
11398
- import { execFileSync as execFileSync5 } from "child_process";
11399
- import { createHash as createHash5 } from "crypto";
11400
- var realExec = (cmd, args, opts) => {
11401
- try {
11402
- return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
11403
- } catch (e) {
11404
- const err3 = e;
11405
- const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
11406
- throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
11407
- }
11408
- };
11409
- var DOCKER_INFO_TIMEOUT_MS = 1e4;
11410
- function dockerAvailable(exec = realExec) {
11411
- try {
11412
- exec("docker", ["info"], { timeoutMs: DOCKER_INFO_TIMEOUT_MS });
11413
- return true;
11414
- } catch {
11415
- return false;
11416
- }
11417
- }
11418
- var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
11419
- function passengerImageRef(cfg) {
11420
- return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
11421
- }
11422
- function imagePresent(image, exec = realExec) {
11423
- try {
11424
- exec("docker", ["image", "inspect", image]);
11425
- return true;
11426
- } catch {
11427
- return false;
12013
+ }
11428
12014
  }
12015
+ return { adopted, interrupted };
11429
12016
  }
11430
- function ensureImage(image, exec = realExec) {
11431
- if (!imagePresent(image, exec)) exec("docker", ["pull", image]);
11432
- }
11433
- function containerNameFor(workspaceId) {
11434
- return `crew-passenger-${workspaceId}`;
11435
- }
11436
- function portFor(workspaceId, base = 28765, span = 1e3) {
11437
- const digest = createHash5("sha256").update(workspaceId).digest();
11438
- return base + digest.readUInt32BE(0) % span;
12017
+
12018
+ // src/live-runs.ts
12019
+ import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
12020
+ import { join as join29 } from "path";
12021
+ function liveRuns(stateDir, isAlive = runIsAlive) {
12022
+ const dir = join29(stateDir, "streams");
12023
+ if (!existsSync25(dir)) return [];
12024
+ const out = [];
12025
+ for (const name of readdirSync11(dir)) {
12026
+ if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
12027
+ const streamPath = join29(dir, name);
12028
+ if (runFinished(streamPath)) continue;
12029
+ const sidecar = readRunSidecar(streamPath);
12030
+ if (!sidecar || !isAlive(sidecar)) continue;
12031
+ out.push({ ...sidecar, streamPath });
12032
+ }
12033
+ return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
11439
12034
  }
11440
- function mountsDigest(mounts, image) {
11441
- const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
11442
- return createHash5("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
12035
+ function siblingRuns(runs, ownTicketRecordId) {
12036
+ return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
11443
12037
  }
11444
- function planContainers(cfg) {
11445
- const byWorkspace = /* @__PURE__ */ new Map();
11446
- const image = passengerImageRef(cfg);
11447
- for (const route2 of cfg.routes) {
11448
- if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
11449
- const workspaceId = route2.resolved.workspaceId;
11450
- const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
11451
- for (const target of passengerCheckoutTargets(route2)) {
11452
- mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
12038
+ async function settleRunEnd(o) {
12039
+ const { tracker, emit } = o;
12040
+ const all = o.runs ?? liveRuns(o.stateDir);
12041
+ const slug2 = routeSlug(o.route.route);
12042
+ const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug2), o.ownTicketRecordId);
12043
+ const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
12044
+ const newest = (rs) => rs[rs.length - 1];
12045
+ if (o.memberId) {
12046
+ try {
12047
+ if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
12048
+ else await tracker.setCrewStatus(o.memberId, "idle");
12049
+ } catch (e) {
12050
+ emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
11453
12051
  }
11454
- byWorkspace.set(workspaceId, mounts);
11455
12052
  }
11456
- return [...byWorkspace.entries()].filter(([, mounts]) => mounts.size > 0).map(([workspaceId, mounts]) => {
11457
- const mountList = [...mounts.values()];
11458
- return {
11459
- workspaceId,
11460
- containerName: containerNameFor(workspaceId),
11461
- port: portFor(workspaceId),
11462
- image,
11463
- mounts: mountList,
11464
- mountsHash: mountsDigest(mountList, image)
11465
- };
11466
- });
11467
- }
11468
- function listPassengerContainers(exec = realExec) {
11469
- const out = exec("docker", ["ps", "-a", "--filter", "name=crew-passenger-", "--format", "{{.Names}} {{.State}}"]);
11470
- return out.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
11471
- const [name, state] = line.split(" ");
11472
- return { name, running: state === "running" };
11473
- });
11474
- }
11475
- function currentMountsHash(name, exec) {
11476
12053
  try {
11477
- const out = exec("docker", ["inspect", "--format", '{{ index .Config.Labels "crew.mounts.hash" }}', name]);
11478
- const hash = out.trim();
11479
- return hash || void 0;
11480
- } catch {
11481
- return void 0;
12054
+ if (shipSiblings.length) {
12055
+ const n = newest(shipSiblings);
12056
+ await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
12057
+ } else {
12058
+ await tracker.beatIdle(o.shipName);
12059
+ }
12060
+ } catch (e) {
12061
+ emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
11482
12062
  }
11483
12063
  }
11484
- function stopContainer(name, exec = realExec) {
12064
+ function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
11485
12065
  try {
11486
- exec("docker", ["rm", "-f", name]);
12066
+ return worktreeForNumber(d, n)?.path ?? null;
11487
12067
  } catch {
12068
+ return null;
11488
12069
  }
11489
- }
11490
- function startContainer(plan, tablationApiBaseUrl, sync, exec = realExec) {
11491
- const args = [
11492
- "run",
11493
- "-d",
11494
- "--name",
11495
- plan.containerName,
11496
- "--restart",
11497
- "unless-stopped",
11498
- "-p",
11499
- `${plan.port}:8765`,
11500
- "-e",
11501
- `PASSENGER_MCP_WORKSPACE_ID=${plan.workspaceId}`,
11502
- "-e",
11503
- `TABLATION_API_BASE_URL=${tablationApiBaseUrl}`,
11504
- "--label",
11505
- `crew.mounts.hash=${plan.mountsHash}`
11506
- ];
11507
- if (sync) {
11508
- args.push(
11509
- "--add-host",
11510
- "host.docker.internal:host-gateway",
11511
- "-e",
11512
- `PASSENGER_SYNC_URL=http://${sync.host}:${sync.port}/sync`,
11513
- "-e",
11514
- `PASSENGER_SYNC_SECRET=${sync.secret}`,
11515
- "-e",
11516
- `PASSENGER_MCP_SECRET=${sync.secret}`
11517
- );
11518
- }
11519
- for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
11520
- args.push(plan.image);
11521
- exec("docker", args);
11522
- }
11523
- function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
11524
- const plans = planContainers(cfg);
11525
- const plannedByName = new Map(plans.map((p) => [p.containerName, p]));
11526
- const existing = listPassengerContainers(exec);
11527
- const existingByName = new Map(existing.map((c) => [c.name, c]));
11528
- const started = [];
11529
- const recreated = [];
11530
- const stopped = [];
11531
- const ensured = /* @__PURE__ */ new Set();
11532
- const start = (plan, sync) => {
11533
- if (!ensured.has(plan.image)) {
11534
- ensureImage(plan.image, exec);
11535
- ensured.add(plan.image);
11536
- }
11537
- startContainer(plan, tablationApiBaseUrl, sync, exec);
12070
+ }) {
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+/, "") ?? "";
12074
+ return {
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,
12080
+ worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
12081
+ stream: run3.streamPath,
12082
+ pid: run3.pid,
12083
+ startedAt: run3.spawnedAt,
12084
+ deadline: run3.deadline ?? null
11538
12085
  };
11539
- for (const plan of plans) {
11540
- const sync = syncEndpoints.get(plan.workspaceId);
11541
- const current = existingByName.get(plan.containerName);
11542
- if (!current) {
11543
- start(plan, sync);
11544
- started.push(plan.containerName);
11545
- } else if (!current.running) {
11546
- stopContainer(plan.containerName, exec);
11547
- start(plan, sync);
11548
- started.push(plan.containerName);
11549
- } else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
11550
- stopContainer(plan.containerName, exec);
11551
- start(plan, sync);
11552
- recreated.push(plan.containerName);
11553
- }
11554
- }
11555
- for (const { name } of existing) {
11556
- if (!plannedByName.has(name)) {
11557
- stopContainer(name, exec);
11558
- stopped.push(name);
11559
- }
11560
- }
11561
- return { started, recreated, stopped };
11562
- }
11563
- function passengerCheckoutSyncSpecs(cfg, workspaceId) {
11564
- const specs = [];
11565
- for (const route2 of cfg.routes) {
11566
- if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
11567
- if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
11568
- const routeWorkspaceId = route2.resolved.workspaceId;
11569
- const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
11570
- for (const target of passengerCheckoutTargets(route2)) {
11571
- const ordinary = ordinaryByName.get(target.name);
11572
- const override = route2.repoOverrides[target.name];
11573
- const effective = resolveRepoConfig(
11574
- ordinary ? loadRepoConfig(ordinary.dir) : null,
11575
- { branch: { ...route2.branch, ...override?.branch } },
11576
- ordinary?.dir ?? target.dir
11577
- );
11578
- specs.push({
11579
- name: target.name,
11580
- workspaceId: routeWorkspaceId,
11581
- dir: target.dir,
11582
- remote: target.remote,
11583
- gitRemoteName: effective.branch.remote,
11584
- base: effective.branch.base,
11585
- originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
11586
- cloneScheme: cfg.ship.cloneScheme,
11587
- ...target.forgeHost ? { forgeHost: target.forgeHost } : {}
11588
- });
11589
- }
11590
- }
11591
- return specs;
11592
- }
11593
- function syncAllPassengerCheckouts(cfg) {
11594
- return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
11595
- name: spec.name,
11596
- workspaceId: spec.workspaceId,
11597
- outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
11598
- }));
11599
12086
  }
11600
12087
 
11601
12088
  // src/deprovision-host.ts
@@ -11629,22 +12116,22 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
11629
12116
  };
11630
12117
  return {
11631
12118
  liveRuns: () => liveRuns(stateDir).map((r) => ({ role: r.role, route: r.route, pid: r.pid, ticketRecordId: r.ticketRecordId ?? void 0, streamPath: r.streamPath })),
11632
- killRun: (run2) => killRunGroup(run2.pid),
11633
- recordKill(run2) {
11634
- 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`, {
11635
12122
  step: "agent",
11636
- role: run2.role,
11637
- data: { event: "run_killed", reason: "deprovision", pid: run2.pid }
12123
+ role: run3.role,
12124
+ data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
11638
12125
  });
11639
12126
  },
11640
- async handBack(run2) {
11641
- const route2 = routeFor(run2.route);
11642
- if (!run2.ticketRecordId || !route2) return;
11643
- 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];
11644
12131
  if (!memberId) return;
11645
- const identity = await resolveSeatKey(route2, run2.role, getStore);
12132
+ const identity = await resolveSeatKey(route2, run3.role, getStore);
11646
12133
  const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
11647
- const t = await tracker.ticket(run2.ticketRecordId);
12134
+ const t = await tracker.ticket(run3.ticketRecordId);
11648
12135
  const target = repoTargetFor(route2, t.repo_id);
11649
12136
  const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
11650
12137
  const wt = target && num ? worktreeForNumber(target.dir, num) : null;
@@ -11653,7 +12140,7 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
11653
12140
  if (wt) dirty = status(wt.path).length > 0;
11654
12141
  } catch {
11655
12142
  }
11656
- await tracker.postEvent(run2.ticketRecordId, interruptedComment({
12143
+ await tracker.postEvent(run3.ticketRecordId, interruptedComment({
11657
12144
  branch: wt?.branch ?? null,
11658
12145
  dirty,
11659
12146
  reason: "the host was released and wiped"
@@ -11708,9 +12195,9 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
11708
12195
  }
11709
12196
 
11710
12197
  // src/pool-member.ts
11711
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync24, renameSync as renameSync3, writeFileSync as writeFileSync17 } from "fs";
12198
+ import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
11712
12199
  import { homedir as homedir8 } from "os";
11713
- import { dirname as dirname11, join as join28 } from "path";
12200
+ import { dirname as dirname11, join as join30 } from "path";
11714
12201
  var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
11715
12202
  function backoffMs(attempt) {
11716
12203
  return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
@@ -11899,24 +12386,24 @@ async function runPoolMember(deps) {
11899
12386
  }
11900
12387
  }
11901
12388
  function defaultMemberDir(env = process.env) {
11902
- return env.CREW_POOL_DIR ?? join28(env.XDG_STATE_HOME ?? join28(homedir8(), ".local", "state"), "crew-pool-member");
12389
+ return env.CREW_POOL_DIR ?? join30(env.XDG_STATE_HOME ?? join30(homedir8(), ".local", "state"), "crew-pool-member");
11903
12390
  }
11904
12391
  function fileMemberStore(dir) {
11905
- const file = join28(dir, "member.json");
12392
+ const file = join30(dir, "member.json");
11906
12393
  return {
11907
12394
  read() {
11908
12395
  try {
11909
- return JSON.parse(readFileSync24(file, "utf8"));
12396
+ return JSON.parse(readFileSync25(file, "utf8"));
11910
12397
  } catch {
11911
12398
  return {};
11912
12399
  }
11913
12400
  },
11914
12401
  write(f) {
11915
- mkdirSync16(dirname11(file), { recursive: true, mode: 448 });
12402
+ mkdirSync17(dirname11(file), { recursive: true, mode: 448 });
11916
12403
  const tmp = `${file}.tmp`;
11917
- writeFileSync17(tmp, `${JSON.stringify(f)}
12404
+ writeFileSync19(tmp, `${JSON.stringify(f)}
11918
12405
  `, { mode: 384 });
11919
- renameSync3(tmp, file);
12406
+ renameSync4(tmp, file);
11920
12407
  }
11921
12408
  };
11922
12409
  }
@@ -11970,18 +12457,18 @@ import { existsSync as existsSync30 } from "fs";
11970
12457
  import { hostname as hostname3 } from "os";
11971
12458
 
11972
12459
  // src/machine-id.ts
11973
- import { execFileSync as execFileSync6 } from "child_process";
11974
- import { createHash as createHash6 } from "crypto";
11975
- import { existsSync as existsSync27, mkdirSync as mkdirSync17, readFileSync as readFileSync25, writeFileSync as writeFileSync18 } from "fs";
11976
- import { dirname as dirname12, join as join29 } from "path";
12460
+ import { execFileSync as execFileSync7 } from "child_process";
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";
11977
12464
  function machineIdPathFor(stateDir) {
11978
- return join29(stateDir, "machine-id");
12465
+ return join31(stateDir, "machine-id");
11979
12466
  }
11980
- var sha256 = (s) => createHash6("sha256").update(s).digest("hex");
12467
+ var sha256 = (s) => createHash7("sha256").update(s).digest("hex");
11981
12468
  function readRawMachineId(platform2 = process.platform) {
11982
12469
  try {
11983
12470
  if (platform2 === "darwin") {
11984
- const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
12471
+ const out = execFileSync7("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
11985
12472
  encoding: "utf8",
11986
12473
  stdio: ["ignore", "pipe", "ignore"]
11987
12474
  });
@@ -11989,7 +12476,7 @@ function readRawMachineId(platform2 = process.platform) {
11989
12476
  }
11990
12477
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
11991
12478
  if (existsSync27(p)) {
11992
- const id = readFileSync25(p, "utf8").trim();
12479
+ const id = readFileSync26(p, "utf8").trim();
11993
12480
  if (id) return id;
11994
12481
  }
11995
12482
  }
@@ -12001,14 +12488,14 @@ function ensureMachineId(stateDir, opts = {}) {
12001
12488
  if (opts.override?.trim()) return sha256(opts.override.trim());
12002
12489
  const path = machineIdPathFor(stateDir);
12003
12490
  if (existsSync27(path)) {
12004
- const cached = readFileSync25(path, "utf8").trim();
12491
+ const cached = readFileSync26(path, "utf8").trim();
12005
12492
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
12006
12493
  }
12007
12494
  const raw = (opts.read ?? readRawMachineId)();
12008
12495
  if (!raw) return void 0;
12009
12496
  const hashed = sha256(raw);
12010
- mkdirSync17(dirname12(path), { recursive: true });
12011
- writeFileSync18(path, `${hashed}
12497
+ mkdirSync18(dirname12(path), { recursive: true });
12498
+ writeFileSync20(path, `${hashed}
12012
12499
  `, { mode: 384 });
12013
12500
  return hashed;
12014
12501
  }
@@ -12021,7 +12508,7 @@ function matchShipRows(ships, shipName2, machineId) {
12021
12508
 
12022
12509
  // src/provision.ts
12023
12510
  import { spawnSync as spawnSync7 } from "child_process";
12024
- import { existsSync as existsSync28, readFileSync as readFileSync26, writeFileSync as writeFileSync19 } from "fs";
12511
+ import { existsSync as existsSync28, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
12025
12512
  import { hostname as hostname2 } from "os";
12026
12513
  import { parseDocument as parseDocument3 } from "yaml";
12027
12514
 
@@ -12030,7 +12517,7 @@ import { parseDocument as parseDocument2 } from "yaml";
12030
12517
 
12031
12518
  // src/repos-cmd.ts
12032
12519
  import { homedir as homedir9 } from "os";
12033
- import { basename as basename8, join as join30, resolve as resolve3 } from "path";
12520
+ import { basename as basename9, join as join32, resolve as resolve3 } from "path";
12034
12521
  import { isMap, isSeq, parseDocument, stringify } from "yaml";
12035
12522
  var ReposError = class extends Error {
12036
12523
  };
@@ -12102,7 +12589,7 @@ function configuredRepos(text2, route2, base) {
12102
12589
  });
12103
12590
  }
12104
12591
  const dir = m.get("dir");
12105
- return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
12592
+ return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
12106
12593
  }
12107
12594
  function cloneBaseOf(text2, route2, base, isCheckout) {
12108
12595
  const m = routeMapOf(parseDocument(text2), route2);
@@ -12191,7 +12678,7 @@ function planRepoAdd(i) {
12191
12678
  throw new ReposError(`${target} is already configured on route ${i.route}`);
12192
12679
  }
12193
12680
  const matched = matchRepoName(i.origin, i.resolved);
12194
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
12681
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
12195
12682
  let warning;
12196
12683
  if (!matched) {
12197
12684
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -12308,7 +12795,7 @@ function planRepoClone(i) {
12308
12795
  }
12309
12796
  const ship = doc.get("ship", true);
12310
12797
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
12311
- const target = i.dir ? expandDir(i.dir, i.base) : join30(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);
12312
12799
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
12313
12800
  const forgeHost = i.resolved?.repoForgeHosts?.[id];
12314
12801
  return { ...add, remote, target, ...forgeHost ? { forgeHost } : {} };
@@ -12581,13 +13068,13 @@ function realProvisionDeps(crewHome) {
12581
13068
  enableRoute(route2) {
12582
13069
  const file = configFile();
12583
13070
  if (!file) return false;
12584
- const text2 = readFileSync26(file, "utf8");
13071
+ const text2 = readFileSync27(file, "utf8");
12585
13072
  const doc = parseDocument3(text2);
12586
13073
  const routes = doc.get("routes");
12587
13074
  const entry = routes?.items?.find((r) => r.get("route") === route2);
12588
13075
  if (!entry) throw new Error(`route ${route2} is not in ${file}`);
12589
13076
  if (entry.get("enabled") === true) return false;
12590
- writeFileSync19(file, planEnable(text2, route2));
13077
+ writeFileSync21(file, planEnable(text2, route2));
12591
13078
  return true;
12592
13079
  },
12593
13080
  installState() {
@@ -12607,7 +13094,7 @@ function realProvisionDeps(crewHome) {
12607
13094
  continue;
12608
13095
  }
12609
13096
  for (const p of plan.unitPaths) {
12610
- if (!existsSync28(p) || readFileSync26(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
13097
+ if (!existsSync28(p) || readFileSync27(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
12611
13098
  }
12612
13099
  }
12613
13100
  const label = labelFor(crewHome, "run");
@@ -12631,10 +13118,10 @@ function realProvisionDeps(crewHome) {
12631
13118
  setShipSetting(key, value2) {
12632
13119
  const file = configFile();
12633
13120
  if (!file) return false;
12634
- const doc = parseDocument3(readFileSync26(file, "utf8"));
13121
+ const doc = parseDocument3(readFileSync27(file, "utf8"));
12635
13122
  if (doc.getIn(["ship", key]) === value2) return false;
12636
13123
  doc.setIn(["ship", key], value2);
12637
- writeFileSync19(file, doc.toString());
13124
+ writeFileSync21(file, doc.toString());
12638
13125
  return true;
12639
13126
  },
12640
13127
  storedToken(host) {
@@ -12650,12 +13137,12 @@ host=${host}
12650
13137
  }
12651
13138
 
12652
13139
  // src/tunnel.ts
12653
- import { spawn as spawn4 } from "child_process";
12654
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync27, writeFileSync as writeFileSync20, 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";
12655
13142
  import { randomInt } from "crypto";
12656
- import { join as join31 } from "path";
13143
+ import { join as join33 } from "path";
12657
13144
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
12658
- var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
13145
+ var defaultSpawn = (cmd, args) => spawn6(cmd, args, DEFAULT_SPAWN_OPTIONS);
12659
13146
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
12660
13147
  var SLUG_LENGTH = 12;
12661
13148
  function generateTunnelSlug() {
@@ -12667,12 +13154,12 @@ function bindAddrFor(o, path) {
12667
13154
  const base = `workspace-${o.workspaceId}-slug-${o.slug}${path ? `-path-${path}` : ""}-key-${o.apiKey}`;
12668
13155
  return o.mcpSecret && !path ? `${base}-secret-${o.mcpSecret}` : base;
12669
13156
  }
12670
- function relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
13157
+ function relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
12671
13158
  const domain = relayPublicDomain ?? relayHost.replace(/^.*@/, "");
12672
- return `https://${slug}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
13159
+ return `https://${slug2}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
12673
13160
  }
12674
- function publicUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
12675
- return `${relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
13161
+ function publicUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
13162
+ return `${relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
12676
13163
  }
12677
13164
  function sshArgsFor(o) {
12678
13165
  const forwards = [];
@@ -12725,21 +13212,21 @@ function startTunnel(opts) {
12725
13212
  };
12726
13213
  }
12727
13214
  function tunnelStatePath(stateDir, workspaceId) {
12728
- return join31(stateDir, "passengers", `${workspaceId}.json`);
13215
+ return join33(stateDir, "passengers", `${workspaceId}.json`);
12729
13216
  }
12730
13217
  function readPersistedTunnel(stateDir, workspaceId) {
12731
13218
  const path = tunnelStatePath(stateDir, workspaceId);
12732
13219
  if (!existsSync29(path)) return void 0;
12733
13220
  try {
12734
- return JSON.parse(readFileSync27(path, "utf8"));
13221
+ return JSON.parse(readFileSync28(path, "utf8"));
12735
13222
  } catch {
12736
13223
  return void 0;
12737
13224
  }
12738
13225
  }
12739
13226
  function writePersistedTunnel(stateDir, state) {
12740
13227
  const path = tunnelStatePath(stateDir, state.workspaceId);
12741
- mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
12742
- writeFileSync20(path, `${JSON.stringify(state, null, 2)}
13228
+ mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
13229
+ writeFileSync22(path, `${JSON.stringify(state, null, 2)}
12743
13230
  `);
12744
13231
  }
12745
13232
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -12773,8 +13260,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12773
13260
  const controlUnchanged = existing?.controlSocket === deps.controlSocket;
12774
13261
  const currentSecret = mcpSecrets.get(plan.workspaceId);
12775
13262
  const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
12776
- const slug = existing?.slug ?? generateTunnelSlug();
12777
- const slugUnchanged = existing?.liveSlug === slug;
13263
+ const slug2 = existing?.slug ?? generateTunnelSlug();
13264
+ const slugUnchanged = existing?.liveSlug === slug2;
12778
13265
  if (existing && alive(existing.pid) && existing.mountsHash === mountsHash && secretUnchanged && slugUnchanged && controlUnchanged) {
12779
13266
  if (existing.status === "connecting") {
12780
13267
  const settled = { ...existing, status: "connected" };
@@ -12799,7 +13286,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12799
13286
  relayHost,
12800
13287
  relayPort,
12801
13288
  workspaceId: plan.workspaceId,
12802
- slug,
13289
+ slug: slug2,
12803
13290
  apiKey,
12804
13291
  localPort: plan.port,
12805
13292
  controlSocket: deps.controlSocket,
@@ -12819,8 +13306,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12819
13306
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
12820
13307
  status: settledStatus,
12821
13308
  mcpSecret: currentSecret,
12822
- slug,
12823
- liveSlug: slug,
13309
+ slug: slug2,
13310
+ liveSlug: slug2,
12824
13311
  endpointId: existing?.endpointId,
12825
13312
  registeredKey: existing?.registeredKey
12826
13313
  };
@@ -12841,7 +13328,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12841
13328
  }
12842
13329
  }
12843
13330
  function readAllPersistedWorkspaceIds(stateDir) {
12844
- const dir = join31(stateDir, "passengers");
13331
+ const dir = join33(stateDir, "passengers");
12845
13332
  if (!existsSync29(dir)) return [];
12846
13333
  return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12847
13334
  }
@@ -12924,8 +13411,8 @@ function realPoolMemberDeps(o) {
12924
13411
  if (!cfg?.ship.relayHost) return null;
12925
13412
  for (const r of cfg.routes) {
12926
13413
  const ws = r.resolved?.workspaceId;
12927
- const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
12928
- 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);
12929
13416
  }
12930
13417
  return null;
12931
13418
  },
@@ -13708,8 +14195,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
13708
14195
  }
13709
14196
 
13710
14197
  // src/poll.ts
13711
- import { writeFileSync as writeFileSync21 } from "fs";
13712
- import { join as join32 } from "path";
14198
+ import { writeFileSync as writeFileSync23 } from "fs";
14199
+ import { join as join34 } from "path";
13713
14200
 
13714
14201
  // src/stalled.ts
13715
14202
  var STALLED_AFTER_CYCLES = 30;
@@ -14016,7 +14503,7 @@ async function decideCycle(o) {
14016
14503
  }
14017
14504
  return decision;
14018
14505
  }
14019
- var digestPath = (stateDir, route2, role, runId) => join32(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
14506
+ var digestPath = (stateDir, route2, role, runId) => join34(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
14020
14507
  function otherRunsRemoved(tickets, own) {
14021
14508
  if (!own) return tickets;
14022
14509
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -14094,7 +14581,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
14094
14581
 
14095
14582
  ${body}` : body;
14096
14583
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
14097
- writeFileSync21(path, text2);
14584
+ writeFileSync23(path, text2);
14098
14585
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
14099
14586
  data: { role, bytes: Buffer.byteLength(text2) }
14100
14587
  });
@@ -14202,7 +14689,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
14202
14689
  }
14203
14690
 
14204
14691
  // src/repo-refresh.ts
14205
- import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync22 } from "fs";
14692
+ import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
14206
14693
  import { dirname as dirname14 } from "path";
14207
14694
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
14208
14695
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
@@ -14233,10 +14720,10 @@ function applyRepoRows(resolved, rows) {
14233
14720
  function persistRepoRows(stateDir, routeName, rows) {
14234
14721
  const path = resolvedPathFor(stateDir, routeName);
14235
14722
  if (!existsSync31(path)) return;
14236
- const raw = JSON.parse(readFileSync28(path, "utf8"));
14723
+ const raw = JSON.parse(readFileSync29(path, "utf8"));
14237
14724
  applyRepoRows(raw, rows);
14238
- mkdirSync19(dirname14(path), { recursive: true });
14239
- writeFileSync22(path, `${JSON.stringify(raw, null, 2)}
14725
+ mkdirSync20(dirname14(path), { recursive: true });
14726
+ writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
14240
14727
  `);
14241
14728
  }
14242
14729
  async function refreshUnknownRepos(o) {
@@ -14686,8 +15173,8 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
14686
15173
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
14687
15174
 
14688
15175
  // src/connect-wizard.ts
14689
- import { delimiter, dirname as dirname15, join as join33 } from "path";
14690
- import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync20, writeFileSync as writeFileSync23 } 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";
14691
15178
  import { stringify as stringify2 } from "yaml";
14692
15179
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
14693
15180
  function shouldRunWizard(opts) {
@@ -14709,7 +15196,7 @@ function defaultIsExecutable(p) {
14709
15196
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
14710
15197
  for (const dir of pathEnv.split(delimiter)) {
14711
15198
  if (!dir) continue;
14712
- const candidate = join33(dir, name);
15199
+ const candidate = join35(dir, name);
14713
15200
  if (isExecutable(candidate)) return candidate;
14714
15201
  }
14715
15202
  return void 0;
@@ -14770,9 +15257,9 @@ routes:
14770
15257
  ${routeBlock}`;
14771
15258
  }
14772
15259
  function writeNewConfig(path, text2) {
14773
- mkdirSync20(dirname15(path), { recursive: true });
15260
+ mkdirSync21(dirname15(path), { recursive: true });
14774
15261
  try {
14775
- writeFileSync23(path, text2, { mode: 384, flag: "wx" });
15262
+ writeFileSync25(path, text2, { mode: 384, flag: "wx" });
14776
15263
  return true;
14777
15264
  } catch (e) {
14778
15265
  if (e.code === "EEXIST") return false;
@@ -14806,7 +15293,7 @@ Wrote ${o.configPath}.
14806
15293
  // src/connect-json.ts
14807
15294
  import { accessSync as accessSync4, constants as constants4 } from "fs";
14808
15295
  import { homedir as homedir10 } from "os";
14809
- import { dirname as dirname16, join as join34 } from "path";
15296
+ import { dirname as dirname16, join as join36 } from "path";
14810
15297
  import { delimiter as delimiter2 } from "path";
14811
15298
  var CHOICE_EXIT_CODE = 3;
14812
15299
  function createEmitter(json2, write) {
@@ -14845,12 +15332,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
14845
15332
  const dirs = [
14846
15333
  ...pathEnv.split(delimiter2),
14847
15334
  ...(extraPath ?? "").split(delimiter2),
14848
- join34(home, ".local/bin"),
14849
- join34(home, ".claude/local"),
15335
+ join36(home, ".local/bin"),
15336
+ join36(home, ".claude/local"),
14850
15337
  "/opt/homebrew/bin",
14851
15338
  "/usr/local/bin",
14852
- join34(home, ".npm-global/bin"),
14853
- join34(home, ".volta/bin")
15339
+ join36(home, ".npm-global/bin"),
15340
+ join36(home, ".volta/bin")
14854
15341
  ];
14855
15342
  return [...new Set(dirs.filter(Boolean))];
14856
15343
  }
@@ -14886,9 +15373,9 @@ function flagWizardAnswers(f, env) {
14886
15373
  }
14887
15374
 
14888
15375
  // src/release-run.ts
14889
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync24, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync2, rmSync as rmSync15 } from "fs";
14890
- import { dirname as dirname17, join as join35 } from "path";
14891
- import { tmpdir as tmpdir3 } 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";
14892
15379
 
14893
15380
  // src/release.ts
14894
15381
  function describeUnplaceable(unplaceable, verifiedStatus) {
@@ -15628,13 +16115,13 @@ async function bump(o, size) {
15628
16115
  }
15629
16116
  return printed;
15630
16117
  }
15631
- const files = o.repo.release.versionFiles.map((f) => join35(o.cwd, f));
16118
+ const files = o.repo.release.versionFiles.map((f) => join37(o.cwd, f));
15632
16119
  const first = files[0];
15633
16120
  if (!existsSync33(first)) {
15634
16121
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
15635
16122
  return null;
15636
16123
  }
15637
- const current = JSON.parse(readFileSync29(first, "utf8")).version;
16124
+ const current = JSON.parse(readFileSync30(first, "utf8")).version;
15638
16125
  if (!current) {
15639
16126
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
15640
16127
  return null;
@@ -15647,27 +16134,27 @@ async function bump(o, size) {
15647
16134
  return version;
15648
16135
  }
15649
16136
  for (const f of files) {
15650
- const src = readFileSync29(f, "utf8");
16137
+ const src = readFileSync30(f, "utf8");
15651
16138
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
15652
16139
  if (out === src) {
15653
16140
  o.emit.error(`no version field in ${f}`);
15654
16141
  return null;
15655
16142
  }
15656
- writeFileSync24(f, out);
16143
+ writeFileSync26(f, out);
15657
16144
  }
15658
16145
  return version;
15659
16146
  }
15660
16147
  function writeChangelog(o, version, entries) {
15661
16148
  const file = o.repo.release.changelog;
15662
16149
  if (!file || entries.length === 0) return;
15663
- const path = join35(o.cwd, file);
16150
+ const path = join37(o.cwd, file);
15664
16151
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
15665
16152
  if (o.dryRun) {
15666
16153
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
15667
16154
  return;
15668
16155
  }
15669
- const existing = existsSync33(path) ? readFileSync29(path, "utf8") : "# Changelog\n";
15670
- writeFileSync24(path, insertChangelogSection(existing, section));
16156
+ const existing = existsSync33(path) ? readFileSync30(path, "utf8") : "# Changelog\n";
16157
+ writeFileSync26(path, insertChangelogSection(existing, section));
15671
16158
  }
15672
16159
  async function confirm(o, expected) {
15673
16160
  if (!o.repo.hooks.released) return void 0;
@@ -15745,23 +16232,23 @@ async function confirmLastRelease(o, decision) {
15745
16232
  if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
15746
16233
  return { confirmed, version };
15747
16234
  }
15748
- function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
16235
+ function closeReleaseCheckout(cwd, root, dir, branch, run3 = gitOk) {
15749
16236
  try {
15750
- run2(cwd, ["worktree", "remove", "--force", dir]);
16237
+ run3(cwd, ["worktree", "remove", "--force", dir]);
15751
16238
  } finally {
15752
16239
  try {
15753
- rmSync15(root, { recursive: true, force: true });
16240
+ rmSync17(root, { recursive: true, force: true });
15754
16241
  } finally {
15755
- run2(cwd, ["worktree", "prune"]);
15756
- run2(cwd, ["branch", "-D", branch]);
16242
+ run3(cwd, ["worktree", "prune"]);
16243
+ run3(cwd, ["branch", "-D", branch]);
15757
16244
  }
15758
16245
  }
15759
16246
  }
15760
16247
  function openReleaseCheckout(o) {
15761
16248
  const { remote, base } = o.repo.branch;
15762
16249
  const branch = `crew/release-${Date.now()}`;
15763
- const root = mkdtempSync2(join35(tmpdir3(), "crew-release-"));
15764
- const dir = join35(root, "checkout");
16250
+ const root = mkdtempSync3(join37(tmpdir5(), "crew-release-"));
16251
+ const dir = join37(root, "checkout");
15765
16252
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
15766
16253
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
15767
16254
  try {
@@ -15771,10 +16258,10 @@ function openReleaseCheckout(o) {
15771
16258
  throw e;
15772
16259
  }
15773
16260
  for (const f of o.repo.worktrees.copy) {
15774
- const src = join35(o.cwd, f);
16261
+ const src = join37(o.cwd, f);
15775
16262
  if (!existsSync33(src)) continue;
15776
- mkdirSync21(dirname17(join35(dir, f)), { recursive: true });
15777
- copyFileSync(src, join35(dir, f));
16263
+ mkdirSync22(dirname17(join37(dir, f)), { recursive: true });
16264
+ copyFileSync(src, join37(dir, f));
15778
16265
  }
15779
16266
  return { dir, branch, close };
15780
16267
  }
@@ -15919,7 +16406,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
15919
16406
  let checkout;
15920
16407
  const stranded = recoverStrandedReleaseCheckouts(o.cwd);
15921
16408
  if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
15922
- const swept = sweepStaleReleaseRoots(tmpdir3());
16409
+ const swept = sweepStaleReleaseRoots(tmpdir5());
15923
16410
  if (swept.length > 0) o.emit.emit(scoped(`removed ${swept.length} stale crew-release-* temp dir(s)`), { step: "release" });
15924
16411
  try {
15925
16412
  checkout = openReleaseCheckout(o);
@@ -17092,7 +17579,7 @@ function nextRoles(decision, ran) {
17092
17579
  }
17093
17580
 
17094
17581
  // src/ship-name.ts
17095
- import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync30, renameSync as renameSync4, writeFileSync as writeFileSync25 } from "fs";
17582
+ import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
17096
17583
  import { dirname as dirname18 } from "path";
17097
17584
  import { parseDocument as parseDocument5 } from "yaml";
17098
17585
  var ShipNameError = class extends Error {
@@ -17156,7 +17643,7 @@ async function syncRouteName(i) {
17156
17643
  function readSyncedName(resolvedFile) {
17157
17644
  if (!existsSync35(resolvedFile)) return void 0;
17158
17645
  try {
17159
- const v = JSON.parse(readFileSync30(resolvedFile, "utf8")).shipName;
17646
+ const v = JSON.parse(readFileSync31(resolvedFile, "utf8")).shipName;
17160
17647
  return typeof v === "string" ? v : void 0;
17161
17648
  } catch {
17162
17649
  return void 0;
@@ -17166,22 +17653,22 @@ function writeSyncedName(resolvedFile, name) {
17166
17653
  if (!existsSync35(resolvedFile)) return;
17167
17654
  let raw;
17168
17655
  try {
17169
- raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
17656
+ raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
17170
17657
  } catch {
17171
17658
  return;
17172
17659
  }
17173
17660
  if (raw.shipName === name) return;
17174
17661
  raw.shipName = name;
17175
- mkdirSync22(dirname18(resolvedFile), { recursive: true });
17662
+ mkdirSync23(dirname18(resolvedFile), { recursive: true });
17176
17663
  const tmp = `${resolvedFile}.tmp${process.pid}`;
17177
- writeFileSync25(tmp, `${JSON.stringify(raw, null, 2)}
17664
+ writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
17178
17665
  `);
17179
- renameSync4(tmp, resolvedFile);
17666
+ renameSync5(tmp, resolvedFile);
17180
17667
  }
17181
17668
  async function reconcileShipName(i) {
17182
17669
  let local = i.ship.name;
17183
17670
  try {
17184
- local = shipNameOfYaml(readFileSync30(i.configFile, "utf8")) ?? local;
17671
+ local = shipNameOfYaml(readFileSync31(i.configFile, "utf8")) ?? local;
17185
17672
  } catch {
17186
17673
  }
17187
17674
  const resolvedFile = resolvedPathFor(i.stateDir, i.route);
@@ -17205,8 +17692,8 @@ async function reconcileShipName(i) {
17205
17692
  }
17206
17693
  if (result.status !== "ok") return result;
17207
17694
  if (result.action === "adopt" || result.action === "row-wins") {
17208
- const text2 = readFileSync30(i.configFile, "utf8");
17209
- writeFileSync25(i.configFile, setShipNameInYaml(text2, result.name));
17695
+ const text2 = readFileSync31(i.configFile, "utf8");
17696
+ writeFileSync27(i.configFile, setShipNameInYaml(text2, result.name));
17210
17697
  i.ship.name = result.name;
17211
17698
  if (result.action === "row-wins") {
17212
17699
  i.emit?.warn(
@@ -17865,7 +18352,7 @@ function describeCapability(i) {
17865
18352
  }
17866
18353
 
17867
18354
  // src/agents-sync-run.ts
17868
- import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync31, writeFileSync as writeFileSync26 } from "fs";
18355
+ import { existsSync as existsSync36, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
17869
18356
  import { dirname as dirname19 } from "path";
17870
18357
  async function runAgentsSync(target, o) {
17871
18358
  const dryRun = o.dryRun ?? false;
@@ -17899,12 +18386,12 @@ async function runAgentsSync(target, o) {
17899
18386
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
17900
18387
  if (!dryRun) {
17901
18388
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
17902
- const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
18389
+ const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync32(resolvedPath, "utf8")) : {};
17903
18390
  raw.agentPersonas = personaResult.agentPersonas;
17904
18391
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
17905
18392
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
17906
- mkdirSync23(dirname19(resolvedPath), { recursive: true });
17907
- writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
18393
+ mkdirSync24(dirname19(resolvedPath), { recursive: true });
18394
+ writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
17908
18395
  `);
17909
18396
  if (target.resolved) {
17910
18397
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -17919,7 +18406,7 @@ async function runAgentsSync(target, o) {
17919
18406
  }
17920
18407
 
17921
18408
  // src/logbook.ts
17922
- import { createHash as createHash7 } from "crypto";
18409
+ import { createHash as createHash8 } from "crypto";
17923
18410
  import { TablationClient as TablationClient4 } from "@tablation/client";
17924
18411
  var LogbookError = class extends Error {
17925
18412
  };
@@ -17992,7 +18479,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
17992
18479
  };
17993
18480
  }
17994
18481
  const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
17995
- const actualSha = createHash7("sha256").update(prompt).digest("hex");
18482
+ const actualSha = createHash8("sha256").update(prompt).digest("hex");
17996
18483
  const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
17997
18484
  return {
17998
18485
  entry,
@@ -18008,31 +18495,31 @@ async function showLogEntry(route2, entryId, opts = {}) {
18008
18495
 
18009
18496
  // src/repos-check.ts
18010
18497
  import { spawnSync as spawnSync9 } from "child_process";
18011
- import { readFileSync as readFileSync32 } from "fs";
18012
- import { basename as basename9 } from "path";
18498
+ import { readFileSync as readFileSync33 } from "fs";
18499
+ import { basename as basename10 } from "path";
18013
18500
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
18014
18501
  function checkHookSyntax(script, shell) {
18015
18502
  const spec = resolveShell(shell);
18016
- const name = basename9(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18017
- let run2;
18503
+ const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18504
+ let run3;
18018
18505
  if (POSIX_SHELL.test(name)) {
18019
- run2 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
18506
+ run3 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
18020
18507
  } else if (/^(pwsh|powershell)$/.test(name)) {
18021
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}";
18022
- run2 = spawnSync9(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
18509
+ run3 = spawnSync9(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
18023
18510
  encoding: "utf8",
18024
18511
  env: { ...process.env, CREW_CHECK_SCRIPT: script }
18025
18512
  });
18026
18513
  } else {
18027
18514
  return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
18028
18515
  }
18029
- if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
18030
- if (run2.status === 0) return { status: "ok" };
18031
- 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}`;
18032
18519
  return { status: "error", message };
18033
18520
  }
18034
18521
  function renderSamples(cfg, dir) {
18035
- const checkout = basename9(dir.replace(/[/\\]+$/, ""));
18522
+ const checkout = basename10(dir.replace(/[/\\]+$/, ""));
18036
18523
  const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
18037
18524
  const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
18038
18525
  return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
@@ -18049,7 +18536,7 @@ function checkRepoSpec(dir, opts = {}) {
18049
18536
  }
18050
18537
  let parsed;
18051
18538
  try {
18052
- parsed = parseRepoConfig(readFileSync32(file, "utf8"), file);
18539
+ parsed = parseRepoConfig(readFileSync33(file, "utf8"), file);
18053
18540
  } catch (e) {
18054
18541
  if (!(e instanceof RepoConfigError)) throw e;
18055
18542
  return { ...empty, file, error: e.message, exitCode: 1 };
@@ -18324,7 +18811,7 @@ function existingRepoConfig(f) {
18324
18811
  }
18325
18812
 
18326
18813
  // src/repos-hooks-agent.ts
18327
- import { spawn as spawn5, execFileSync as execFileSync7 } from "child_process";
18814
+ import { spawn as spawn7, execFileSync as execFileSync8 } from "child_process";
18328
18815
  import { realpathSync as realpathSync2 } from "fs";
18329
18816
  import { isAbsolute as isAbsolute2, relative } from "path";
18330
18817
  var AgentSuggestError = class extends Error {
@@ -18427,7 +18914,7 @@ function classifyFailure(o) {
18427
18914
  }
18428
18915
  function porcelain(dir) {
18429
18916
  try {
18430
- return execFileSync7("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
18917
+ return execFileSync8("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
18431
18918
  } catch {
18432
18919
  return void 0;
18433
18920
  }
@@ -18442,10 +18929,10 @@ async function runAgentSuggest(o) {
18442
18929
  const before = porcelain(o.dir);
18443
18930
  const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
18444
18931
  const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
18445
- const run2 = await new Promise((resolve7) => {
18932
+ const run3 = await new Promise((resolve7) => {
18446
18933
  let out = "";
18447
18934
  let err3 = "";
18448
- const child = spawn5(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"] });
18449
18936
  let settled = false;
18450
18937
  const settle = (r) => {
18451
18938
  if (settled) return;
@@ -18477,18 +18964,18 @@ async function runAgentSuggest(o) {
18477
18964
  });
18478
18965
  child.stdin.end(buildAgentPrompt(o.sniff));
18479
18966
  });
18480
- if (run2.aborted || signal.aborted) {
18967
+ if (run3.aborted || signal.aborted) {
18481
18968
  throw new AgentSuggestError(o.signal?.aborted ? "cancelled" : "timeout", o.signal?.aborted ? "cancelled" : "the toolchain did not answer in time");
18482
18969
  }
18483
18970
  const after = porcelain(o.dir);
18484
18971
  if (before !== void 0 && after !== before) {
18485
18972
  throw new AgentSuggestError("modified_checkout", "the checkout changed during --agent; inspect it with `git status` (the agent result was discarded)");
18486
18973
  }
18487
- const parsed = parseAgentStream(run2.out, o.dir);
18488
- if (run2.spawnError || run2.code !== 0 || parsed.isError) {
18489
- const reason = classifyFailure({ code: run2.code, ...run2.spawnError ? { spawnError: run2.spawnError } : {}, output: `${parsed.text}
18490
- ${run2.err}` });
18491
- 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}`);
18492
18979
  }
18493
18980
  const { hooks, notes } = parseAgentHooks(parsed.text);
18494
18981
  const differsFromSniff = HOOK_KEYS2.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
@@ -18496,7 +18983,7 @@ ${run2.err}` });
18496
18983
  }
18497
18984
 
18498
18985
  // src/sample-ticket.ts
18499
- import { basename as basename10, dirname as dirname20, join as join36 } from "path";
18986
+ import { basename as basename11, dirname as dirname20, join as join38 } from "path";
18500
18987
  var SAMPLE_REPO = "crew-sample";
18501
18988
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
18502
18989
  var SAMPLE_MARKER = "sample: true";
@@ -18519,8 +19006,8 @@ has something safe to work on: its test and build hooks always pass, it has no
18519
19006
  remote, and \`crew sample-ticket --remove\` deletes it.
18520
19007
  `;
18521
19008
  function sampleDir(existing, reposBase, workspace) {
18522
- if (existing.length > 0) return join36(dirname20(existing[0]), SAMPLE_REPO);
18523
- return join36(reposBase, workspace, SAMPLE_REPO);
19009
+ if (existing.length > 0) return join38(dirname20(existing[0]), SAMPLE_REPO);
19010
+ return join38(reposBase, workspace, SAMPLE_REPO);
18524
19011
  }
18525
19012
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
18526
19013
  function sampleDescription() {
@@ -18564,8 +19051,8 @@ function planSampleTicket(i) {
18564
19051
  }
18565
19052
 
18566
19053
  // src/worktree-sweep.ts
18567
- import { existsSync as existsSync37, readFileSync as readFileSync33, readdirSync as readdirSync14 } from "fs";
18568
- import { basename as basename11, dirname as dirname21, join as join37, 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";
18569
19056
  function planWorktreeSweep(target, tickets, contract) {
18570
19057
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
18571
19058
  const verified = contract.statuses.verified;
@@ -18606,7 +19093,7 @@ function ticketForWorktree(w, byNumber, contract) {
18606
19093
  const t = num ? byNumber.get(num) : void 0;
18607
19094
  if (t && ticketStems(t).has(stem)) return t;
18608
19095
  }
18609
- const dir = basename11(w.path);
19096
+ const dir = basename12(w.path);
18610
19097
  const m = DIR_NUMBER.exec(dir);
18611
19098
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
18612
19099
  if (!w.branch) {
@@ -18622,7 +19109,7 @@ function worktreeTicketNumbers(target) {
18622
19109
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
18623
19110
  if (n) nums.add(n);
18624
19111
  }
18625
- const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
19112
+ const m = DIR_NUMBER_SUFFIXED.exec(basename12(w.path));
18626
19113
  if (m) nums.add(m[1]);
18627
19114
  }
18628
19115
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
@@ -18630,7 +19117,7 @@ function worktreeTicketNumbers(target) {
18630
19117
  }
18631
19118
  function siblingWorktreeDirs(target) {
18632
19119
  const parent = dirname21(target.dir.replace(/[/\\]+$/, ""));
18633
- const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
19120
+ const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
18634
19121
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
18635
19122
  let names;
18636
19123
  try {
@@ -18642,7 +19129,7 @@ function siblingWorktreeDirs(target) {
18642
19129
  for (const name of names) {
18643
19130
  if (!name.startsWith(stem)) continue;
18644
19131
  const m = DIR_NUMBER_SUFFIXED.exec(name);
18645
- const path = join37(parent, name);
19132
+ const path = join39(parent, name);
18646
19133
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
18647
19134
  }
18648
19135
  return out;
@@ -18655,7 +19142,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
18655
19142
  }
18656
19143
  function warnOrphanedWorktreeDirs(paths, log, held) {
18657
19144
  for (const p of paths) {
18658
- const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
19145
+ const number = DIR_NUMBER_SUFFIXED.exec(basename12(p))?.[1];
18659
19146
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
18660
19147
  if (when) {
18661
19148
  log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
@@ -18745,7 +19232,7 @@ function isHandBackMerge(path, base) {
18745
19232
  if (!msgPath) return false;
18746
19233
  let first;
18747
19234
  try {
18748
- first = readFileSync33(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
19235
+ first = readFileSync34(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
18749
19236
  } catch {
18750
19237
  return false;
18751
19238
  }
@@ -18799,7 +19286,7 @@ function isTicketBranch(name, base, stems) {
18799
19286
  }
18800
19287
  function sweepCandidatesExist(cwd, base) {
18801
19288
  const linked = worktrees(cwd).slice(1);
18802
- if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename11(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
19289
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename12(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
18803
19290
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
18804
19291
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
18805
19292
  }
@@ -18850,14 +19337,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
18850
19337
  }
18851
19338
 
18852
19339
  // src/stream-sweep.ts
18853
- import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync16, statSync as statSync10 } from "fs";
18854
- import { join as join38 } 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";
18855
19342
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
18856
19343
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
18857
- const dir = join38(stateDir, "streams");
19344
+ const dir = join40(stateDir, "streams");
18858
19345
  if (!existsSync38(dir)) return [];
18859
19346
  const cutoff = now - retentionDays * DAY_MS2;
18860
- return readdirSync15(dir).map((name) => join38(dir, name)).filter((path) => {
19347
+ return readdirSync15(dir).map((name) => join40(dir, name)).filter((path) => {
18861
19348
  try {
18862
19349
  return statSync10(path).mtimeMs < cutoff;
18863
19350
  } catch {
@@ -18873,7 +19360,7 @@ function applyStreamSweep(paths, dryRun, log) {
18873
19360
  continue;
18874
19361
  }
18875
19362
  try {
18876
- rmSync16(path, { force: true });
19363
+ rmSync18(path, { force: true });
18877
19364
  removed++;
18878
19365
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
18879
19366
  } catch (e) {
@@ -18885,7 +19372,7 @@ function applyStreamSweep(paths, dryRun, log) {
18885
19372
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
18886
19373
  if (!existsSync38(stateDir)) return [];
18887
19374
  const cutoff = now - retentionDays * DAY_MS2;
18888
- return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join38(stateDir, name)).filter((path) => {
19375
+ return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join40(stateDir, name)).filter((path) => {
18889
19376
  try {
18890
19377
  return statSync10(path).mtimeMs < cutoff;
18891
19378
  } catch {
@@ -18895,12 +19382,12 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
18895
19382
  }
18896
19383
 
18897
19384
  // src/cli.ts
18898
- import { realpathSync as realpathSync3, readFileSync as readFileSync39, existsSync as existsSync45, mkdirSync as mkdirSync29, writeFileSync as writeFileSync31, rmSync as rmSync21, 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";
18899
19386
  import { createInterface } from "readline/promises";
18900
19387
 
18901
19388
  // src/periscope-cmd.ts
18902
- import { existsSync as existsSync39, mkdirSync as mkdirSync24, readFileSync as readFileSync34, rmSync as rmSync17, writeFileSync as writeFileSync27 } from "fs";
18903
- import { join as join39 } 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";
18904
19391
  var PeriscopeError = class extends Error {
18905
19392
  };
18906
19393
  var LivePeriscopeError = class extends PeriscopeError {
@@ -18910,22 +19397,22 @@ var LivePeriscopeError = class extends PeriscopeError {
18910
19397
  this.data = { refused: "live_periscope_exists", ...live };
18911
19398
  }
18912
19399
  };
18913
- var priorDir = (stateDir) => join39(stateDir, "periscope-priors");
18914
- var priorPath = (stateDir, ticketRecordId) => join39(priorDir(stateDir), `${ticketRecordId}.json`);
19400
+ var priorDir = (stateDir) => join41(stateDir, "periscope-priors");
19401
+ var priorPath = (stateDir, ticketRecordId) => join41(priorDir(stateDir), `${ticketRecordId}.json`);
18915
19402
  function removePeriscopePrior(stateDir, ticketRecordId) {
18916
- rmSync17(priorPath(stateDir, ticketRecordId), { force: true });
19403
+ rmSync19(priorPath(stateDir, ticketRecordId), { force: true });
18917
19404
  }
18918
19405
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
18919
19406
  try {
18920
- mkdirSync24(priorDir(stateDir), { recursive: true });
18921
- writeFileSync27(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
19407
+ mkdirSync25(priorDir(stateDir), { recursive: true });
19408
+ writeFileSync29(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
18922
19409
  } catch {
18923
19410
  }
18924
19411
  }
18925
19412
  function readPeriscopePrior(stateDir, ticketRecordId) {
18926
19413
  try {
18927
19414
  const p = priorPath(stateDir, ticketRecordId);
18928
- return existsSync39(p) ? JSON.parse(readFileSync34(p, "utf8")) : void 0;
19415
+ return existsSync39(p) ? JSON.parse(readFileSync35(p, "utf8")) : void 0;
18929
19416
  } catch {
18930
19417
  return void 0;
18931
19418
  }
@@ -19322,14 +19809,14 @@ ${tail2(preview.output)}`);
19322
19809
  }
19323
19810
 
19324
19811
  // src/periscope-idle.ts
19325
- import { execFileSync as execFileSync8 } from "child_process";
19812
+ import { execFileSync as execFileSync9 } from "child_process";
19326
19813
  import { statSync as statSync11 } from "fs";
19327
- import { join as join40 } from "path";
19814
+ import { join as join42 } from "path";
19328
19815
  var MAX_SCANNED_FILES = 2e4;
19329
19816
  function newestWorktreeMtime(root) {
19330
19817
  let listing;
19331
19818
  try {
19332
- listing = execFileSync8("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19819
+ listing = execFileSync9("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19333
19820
  cwd: root,
19334
19821
  encoding: "utf8",
19335
19822
  stdio: ["ignore", "pipe", "ignore"],
@@ -19344,7 +19831,7 @@ function newestWorktreeMtime(root) {
19344
19831
  if (!rel) continue;
19345
19832
  if (++scanned > MAX_SCANNED_FILES) break;
19346
19833
  try {
19347
- const m = statSync11(join40(root, rel)).mtimeMs;
19834
+ const m = statSync11(join42(root, rel)).mtimeMs;
19348
19835
  if (newest === null || m > newest) newest = m;
19349
19836
  } catch {
19350
19837
  }
@@ -19419,21 +19906,21 @@ async function runPeriscopeIdlePass(d) {
19419
19906
  import { dirname as dirOf2, resolve as resolvePath } from "path";
19420
19907
 
19421
19908
  // src/ssh-keys.ts
19422
- import { execFileSync as execFileSync9 } from "child_process";
19423
- import { existsSync as existsSync40, mkdirSync as mkdirSync25, readFileSync as readFileSync35, chmodSync as chmodSync3 } from "fs";
19424
- import { dirname as dirname22, join as join41 } from "path";
19909
+ import { execFileSync as execFileSync10 } from "child_process";
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";
19425
19912
  var SshKeyError = class extends Error {
19426
19913
  };
19427
19914
  function shipSshKeyPathFor(stateDir) {
19428
- return join41(stateDir, "ssh", "id_ed25519");
19915
+ return join43(stateDir, "ssh", "id_ed25519");
19429
19916
  }
19430
19917
  function ensureShipSshKeypair(stateDir) {
19431
19918
  const privateKeyPath = shipSshKeyPathFor(stateDir);
19432
19919
  const publicKeyPath = `${privateKeyPath}.pub`;
19433
19920
  if (!existsSync40(privateKeyPath)) {
19434
- mkdirSync25(dirname22(privateKeyPath), { recursive: true });
19921
+ mkdirSync26(dirname22(privateKeyPath), { recursive: true });
19435
19922
  try {
19436
- execFileSync9("ssh-keygen", [
19923
+ execFileSync10("ssh-keygen", [
19437
19924
  "-t",
19438
19925
  "ed25519",
19439
19926
  "-N",
@@ -19454,12 +19941,12 @@ function ensureShipSshKeypair(stateDir) {
19454
19941
  if (!existsSync40(publicKeyPath)) {
19455
19942
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
19456
19943
  }
19457
- const publicKey = readFileSync35(publicKeyPath, "utf8").trim();
19944
+ const publicKey = readFileSync36(publicKeyPath, "utf8").trim();
19458
19945
  return { privateKeyPath, publicKeyPath, publicKey };
19459
19946
  }
19460
19947
  function sshKeygenAvailable() {
19461
19948
  try {
19462
- execFileSync9("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
19949
+ execFileSync10("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
19463
19950
  return true;
19464
19951
  } catch (e) {
19465
19952
  return e.code !== "ENOENT";
@@ -19497,7 +19984,7 @@ function shipIdentityJson(s) {
19497
19984
  }
19498
19985
 
19499
19986
  // src/mcp-endpoint-registry.ts
19500
- import { createHash as createHash8 } from "crypto";
19987
+ import { createHash as createHash9 } from "crypto";
19501
19988
  function endpointsUrl(api, workspaceId, id) {
19502
19989
  const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
19503
19990
  return id ? `${base}/${id}` : base;
@@ -19567,7 +20054,7 @@ async function probeEndpointAccess(api, workspaceId) {
19567
20054
  }
19568
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";
19569
20056
  function registrationKey(url, mcpSecret, controlUrl) {
19570
- return createHash8("sha256").update(`${url}
20057
+ return createHash9("sha256").update(`${url}
19571
20058
  ${mcpSecret ?? ""}
19572
20059
  ${controlUrl ?? ""}`).digest("hex").slice(0, 16);
19573
20060
  }
@@ -19616,10 +20103,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
19616
20103
 
19617
20104
  // src/passenger-sync-daemon.ts
19618
20105
  import { createServer as createHttpServer } from "http";
19619
- import { spawn as spawn6 } from "child_process";
20106
+ import { spawn as spawn8 } from "child_process";
19620
20107
  import { randomBytes } from "crypto";
19621
- import { existsSync as existsSync41, mkdirSync as mkdirSync26, readFileSync as readFileSync36, writeFileSync as writeFileSync28, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
19622
- import { join as join42 } 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";
19623
20110
 
19624
20111
  // src/secret-compare.ts
19625
20112
  import { timingSafeEqual } from "crypto";
@@ -19703,26 +20190,26 @@ function runSyncDaemonFromEnv(env = process.env) {
19703
20190
  return server;
19704
20191
  }
19705
20192
  var defaultSpawn2 = (cmd, args, env) => {
19706
- const child = spawn6(cmd, args, { detached: true, stdio: "ignore", env });
20193
+ const child = spawn8(cmd, args, { detached: true, stdio: "ignore", env });
19707
20194
  child.unref();
19708
20195
  return child;
19709
20196
  };
19710
20197
  function syncDaemonStatePath(stateDir, workspaceId) {
19711
- return join42(stateDir, "passengers", `${workspaceId}-sync.json`);
20198
+ return join44(stateDir, "passengers", `${workspaceId}-sync.json`);
19712
20199
  }
19713
20200
  function readPersistedSyncDaemon(stateDir, workspaceId) {
19714
20201
  const path = syncDaemonStatePath(stateDir, workspaceId);
19715
20202
  if (!existsSync41(path)) return void 0;
19716
20203
  try {
19717
- return JSON.parse(readFileSync36(path, "utf8"));
20204
+ return JSON.parse(readFileSync37(path, "utf8"));
19718
20205
  } catch {
19719
20206
  return void 0;
19720
20207
  }
19721
20208
  }
19722
20209
  function writePersistedSyncDaemon(stateDir, state) {
19723
20210
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
19724
- mkdirSync26(join42(stateDir, "passengers"), { recursive: true });
19725
- writeFileSync28(path, `${JSON.stringify(state, null, 2)}
20211
+ mkdirSync27(join44(stateDir, "passengers"), { recursive: true });
20212
+ writeFileSync30(path, `${JSON.stringify(state, null, 2)}
19726
20213
  `);
19727
20214
  }
19728
20215
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -19742,7 +20229,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
19742
20229
  }
19743
20230
  }
19744
20231
  function readAllPersistedSyncWorkspaceIds(stateDir) {
19745
- const dir = join42(stateDir, "passengers");
20232
+ const dir = join44(stateDir, "passengers");
19746
20233
  if (!existsSync41(dir)) return [];
19747
20234
  const suffix = "-sync.json";
19748
20235
  return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -19831,8 +20318,8 @@ function describeCause(err3) {
19831
20318
  }
19832
20319
 
19833
20320
  // src/daemon.ts
19834
- import { existsSync as existsSync42, mkdirSync as mkdirSync27, readFileSync as readFileSync37, readdirSync as readdirSync17, rmSync as rmSync18, statSync as statSync12, writeFileSync as writeFileSync29 } from "fs";
19835
- import { join as join43 } 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";
19836
20323
  import { randomBytes as randomBytes2 } from "crypto";
19837
20324
  var DEFAULT_FLOOR_MS = 5e3;
19838
20325
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -19920,7 +20407,7 @@ function latestMtime(path) {
19920
20407
  if (!stat2.isDirectory()) return 0;
19921
20408
  let latest = stat2.mtimeMs;
19922
20409
  for (const entry of readdirSync17(path, { withFileTypes: true })) {
19923
- latest = Math.max(latest, latestMtime(join43(path, entry.name)));
20410
+ latest = Math.max(latest, latestMtime(join45(path, entry.name)));
19924
20411
  }
19925
20412
  return latest;
19926
20413
  }
@@ -19932,7 +20419,7 @@ function makeStaleChecker(paths, opts = {}) {
19932
20419
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync42(opts.reloadSentinel);
19933
20420
  }
19934
20421
  function reloadSentinelFor(stateDir) {
19935
- return join43(stateDir, "reload-requested");
20422
+ return join45(stateDir, "reload-requested");
19936
20423
  }
19937
20424
  function readReloadRequestedAt(stateDir) {
19938
20425
  try {
@@ -19942,17 +20429,17 @@ function readReloadRequestedAt(stateDir) {
19942
20429
  }
19943
20430
  }
19944
20431
  function daemonStampFor(stateDir) {
19945
- return join43(stateDir, "daemon.started");
20432
+ return join45(stateDir, "daemon.started");
19946
20433
  }
19947
20434
  function noteDaemonStarted(stateDir, now = Date.now()) {
19948
- mkdirSync27(stateDir, { recursive: true });
19949
- rmSync18(reloadSentinelFor(stateDir), { force: true });
19950
- writeFileSync29(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 })}
19951
20438
  `);
19952
20439
  }
19953
20440
  function readDaemonStarted(stateDir) {
19954
20441
  try {
19955
- const v = JSON.parse(readFileSync37(daemonStampFor(stateDir), "utf8")).startedAt;
20442
+ const v = JSON.parse(readFileSync38(daemonStampFor(stateDir), "utf8")).startedAt;
19956
20443
  return typeof v === "number" ? v : null;
19957
20444
  } catch {
19958
20445
  return null;
@@ -20017,7 +20504,7 @@ async function runOnePassUnguarded(o) {
20017
20504
  await hydrateApiKeys([o.route], o.getSessionStore);
20018
20505
  const emit = o.newEmitter();
20019
20506
  const decide = o.decide ?? decideCycle;
20020
- const spawn9 = o.spawnRoleAgent ?? runRoleAgent;
20507
+ const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
20021
20508
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
20022
20509
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
20023
20510
  if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
@@ -20121,7 +20608,7 @@ async function runOnePassUnguarded(o) {
20121
20608
  settle = resolve7;
20122
20609
  });
20123
20610
  const ready = (tookTicket2) => settle(tookTicket2);
20124
- const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
20611
+ const run3 = spawn11(role, decision, ctx, { ready }).then(() => {
20125
20612
  breaker.recordSuccess(role);
20126
20613
  clearShipAttention(o.state, `role_parked:${role}`);
20127
20614
  clearShipAttention(o.state, workspaceAuthKey(role));
@@ -20170,8 +20657,8 @@ async function runOnePassUnguarded(o) {
20170
20657
  });
20171
20658
  if (o.inFlight) {
20172
20659
  const set = o.inFlight;
20173
- set.add(run2);
20174
- void run2.finally(() => set.delete(run2));
20660
+ set.add(run3);
20661
+ void run3.finally(() => set.delete(run3));
20175
20662
  }
20176
20663
  const reported = TICKET_ROLES.has(role) ? await settled : true;
20177
20664
  const tookTicket = reported !== false;
@@ -20507,7 +20994,7 @@ async function buildEnvironment(route2, ship, ticket) {
20507
20994
  }
20508
20995
 
20509
20996
  // src/control-api.ts
20510
- import { createHash as createHash9 } from "crypto";
20997
+ import { createHash as createHash10 } from "crypto";
20511
20998
  import {
20512
20999
  chmodSync as chmodSync4,
20513
21000
  closeSync as closeSync5,
@@ -20516,17 +21003,17 @@ import {
20516
21003
  openSync as openSync5,
20517
21004
  readSync as readSync3,
20518
21005
  readdirSync as readdirSync18,
20519
- rmSync as rmSync19,
21006
+ rmSync as rmSync21,
20520
21007
  statSync as statSync13,
20521
21008
  watch
20522
21009
  } from "fs";
20523
21010
  import { createConnection } from "net";
20524
21011
  import { createServer } from "http";
20525
- import { basename as basename12, dirname as dirname23, join as join44 } from "path";
20526
- import { execFile, spawn as spawn8 } 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";
20527
21014
 
20528
21015
  // src/periscope-control.ts
20529
- import { spawn as spawn7 } from "child_process";
21016
+ import { spawn as spawn9 } from "child_process";
20530
21017
  var reply = (status2, body) => ({ status: status2, body });
20531
21018
  var err = (status2, message) => reply(status2, { error: message });
20532
21019
  var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
@@ -20717,7 +21204,7 @@ async function handlePeriscopes(h, q) {
20717
21204
  }
20718
21205
  function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20719
21206
  return new Promise((resolve7) => {
20720
- const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
21207
+ const child = spawn9(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
20721
21208
  let stdout = "";
20722
21209
  let stderr = "";
20723
21210
  let pending = "";
@@ -20751,7 +21238,7 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20751
21238
  // src/control-api.ts
20752
21239
  var PAUSABLE = ["dev", "design", "qa"];
20753
21240
  var CONTROL_SOCKET_NAME = "control.sock";
20754
- var controlSocketFor = (stateDir) => join44(stateDir, CONTROL_SOCKET_NAME);
21241
+ var controlSocketFor = (stateDir) => join46(stateDir, CONTROL_SOCKET_NAME);
20755
21242
  var MAX_SOCKET_PATH = 100;
20756
21243
  var MAX_TAIL_BYTES = 1024 * 1024;
20757
21244
  var MAX_WAIT_SECONDS = 30;
@@ -20767,7 +21254,7 @@ var ControlAuth = class {
20767
21254
  if (!token) return "unauthorized";
20768
21255
  for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
20769
21256
  const now = (this.o.now ?? Date.now)();
20770
- const id = createHash9("sha256").update(token).digest("hex");
21257
+ const id = createHash10("sha256").update(token).digest("hex");
20771
21258
  const hit = this.cache.get(id);
20772
21259
  if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
20773
21260
  const verdict = await this.ask(token);
@@ -20788,7 +21275,7 @@ var ControlAuth = class {
20788
21275
  for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
20789
21276
  for (const k of this.o.workspaceSecrets?.(workspaceId) ?? []) if (k && secretsMatch(token, k)) return "ok";
20790
21277
  const now = (this.o.now ?? Date.now)();
20791
- const id = `${workspaceId}:${createHash9("sha256").update(token).digest("hex")}`;
21278
+ const id = `${workspaceId}:${createHash10("sha256").update(token).digest("hex")}`;
20792
21279
  const hit = this.cache.get(id);
20793
21280
  if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
20794
21281
  const verdict = await this.ask(token, [workspaceId]);
@@ -20838,7 +21325,7 @@ function candidates(live, rotated) {
20838
21325
  const out = [live];
20839
21326
  if (!rotated) return out;
20840
21327
  try {
20841
- for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join44(dirname23(live), n));
21328
+ for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname23(live), n));
20842
21329
  } catch {
20843
21330
  }
20844
21331
  return out;
@@ -20961,7 +21448,7 @@ function pauseDoc(state) {
20961
21448
  }
20962
21449
  function describeStream(r) {
20963
21450
  return {
20964
- run: basename12(r.streamPath).replace(/\.jsonl$/, ""),
21451
+ run: basename13(r.streamPath).replace(/\.jsonl$/, ""),
20965
21452
  pid: r.pid,
20966
21453
  ticket: r.ticket,
20967
21454
  role: r.role,
@@ -21036,9 +21523,9 @@ async function route(deps, method, url, req, abort) {
21036
21523
  if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
21037
21524
  if (p === "/streams" && get2) return reply2(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
21038
21525
  if (p.startsWith("/streams/") && get2) {
21039
- const run2 = decodeURIComponent(p.slice("/streams/".length));
21040
- if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
21041
- const file = join44(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`);
21042
21529
  if (!existsSync43(file)) return err2(404, "no such run");
21043
21530
  return tailReply(file, url.searchParams, false, abort);
21044
21531
  }
@@ -21101,7 +21588,7 @@ async function startControlServer(deps) {
21101
21588
  log(`control API not started: socket path is too long for a unix socket (${path})`);
21102
21589
  return null;
21103
21590
  }
21104
- if (await socketIsStale(path)) rmSync19(path, { force: true });
21591
+ if (await socketIsStale(path)) rmSync21(path, { force: true });
21105
21592
  else if (existsSync43(path)) {
21106
21593
  log(`control API not started: ${path} is in use by another process`);
21107
21594
  return null;
@@ -21148,7 +21635,7 @@ async function startControlServer(deps) {
21148
21635
  path,
21149
21636
  close: () => new Promise((done) => {
21150
21637
  server.close(() => {
21151
- rmSync19(path, { force: true });
21638
+ rmSync21(path, { force: true });
21152
21639
  done();
21153
21640
  });
21154
21641
  })
@@ -21157,25 +21644,25 @@ async function startControlServer(deps) {
21157
21644
  });
21158
21645
  }
21159
21646
  var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
21160
- function runDetached(commands, spawnFn = spawn8) {
21647
+ function runDetached(commands, spawnFn = spawn10) {
21161
21648
  const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
21162
21649
  const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
21163
21650
  child.on("error", () => {
21164
21651
  });
21165
21652
  child.unref();
21166
21653
  }
21167
- function schedulerHandler(control, run2 = runDetached) {
21654
+ function schedulerHandler(control, run3 = runDetached) {
21168
21655
  return (action) => {
21169
21656
  const { startCommand: start, stopCommand: stop } = control;
21170
21657
  if (!start || !stop) return { accepted: false, detail: "this host has no service manager for the daemon (cron or none)" };
21171
21658
  const commands = action === "start" ? [start] : action === "stop" ? [stop] : [stop, start];
21172
- run2(commands);
21659
+ run3(commands);
21173
21660
  return { accepted: true, detail: `${action} requested` };
21174
21661
  };
21175
21662
  }
21176
21663
  function crewJson(args, timeoutMs = 6e4) {
21177
21664
  return new Promise((resolve7) => {
21178
- execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
21665
+ execFile2(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
21179
21666
  try {
21180
21667
  const v = JSON.parse(String(stdout));
21181
21668
  resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
@@ -21188,53 +21675,53 @@ function crewJson(args, timeoutMs = 6e4) {
21188
21675
 
21189
21676
  // src/periscope-mcp.ts
21190
21677
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
21191
- import { basename as basename13, dirname as dirname24, isAbsolute as isAbsolute3, join as join46, 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";
21192
21679
  import { z } from "zod";
21193
21680
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21194
21681
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21195
21682
 
21196
21683
  // src/periscope-endpoints.ts
21197
21684
  import { randomBytes as randomBytes3 } from "crypto";
21198
- import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync28, readdirSync as readdirSync19, readFileSync as readFileSync38, rmSync as rmSync20, writeFileSync as writeFileSync30 } from "fs";
21199
- import { join as join45 } from "path";
21200
- var secretDir = (stateDir) => join45(stateDir, "periscope-mcp");
21201
- var secretFile = (stateDir, id) => join45(secretDir(stateDir), `${id}.json`);
21202
- var endpointDir = (stateDir) => join45(stateDir, "periscope-endpoints");
21203
- var endpointFile = (stateDir, id) => join45(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`);
21204
21691
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
21205
- mkdirSync28(secretDir(stateDir), { recursive: true, mode: 448 });
21692
+ mkdirSync29(secretDir(stateDir), { recursive: true, mode: 448 });
21206
21693
  const secret = randomBytes3(32).toString("base64url");
21207
21694
  const file = secretFile(stateDir, ticketRecordId);
21208
- writeFileSync30(file, `${JSON.stringify({ secret })}
21695
+ writeFileSync32(file, `${JSON.stringify({ secret })}
21209
21696
  `, { mode: 384 });
21210
21697
  chmodSync5(file, 384);
21211
21698
  return secret;
21212
21699
  }
21213
21700
  function readPeriscopeSecret(stateDir, ticketRecordId) {
21214
21701
  try {
21215
- const v = JSON.parse(readFileSync38(secretFile(stateDir, ticketRecordId), "utf8"));
21702
+ const v = JSON.parse(readFileSync39(secretFile(stateDir, ticketRecordId), "utf8"));
21216
21703
  return typeof v.secret === "string" && v.secret ? v.secret : void 0;
21217
21704
  } catch {
21218
21705
  return void 0;
21219
21706
  }
21220
21707
  }
21221
21708
  function removePeriscopeSecret(stateDir, ticketRecordId) {
21222
- rmSync20(secretFile(stateDir, ticketRecordId), { force: true });
21709
+ rmSync22(secretFile(stateDir, ticketRecordId), { force: true });
21223
21710
  }
21224
21711
  function readPeriscopeEndpoint(stateDir, ticketRecordId) {
21225
21712
  try {
21226
- return JSON.parse(readFileSync38(endpointFile(stateDir, ticketRecordId), "utf8"));
21713
+ return JSON.parse(readFileSync39(endpointFile(stateDir, ticketRecordId), "utf8"));
21227
21714
  } catch {
21228
21715
  return void 0;
21229
21716
  }
21230
21717
  }
21231
21718
  function writePeriscopeEndpoint(stateDir, rec) {
21232
- mkdirSync28(endpointDir(stateDir), { recursive: true });
21233
- writeFileSync30(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)}
21234
21721
  `);
21235
21722
  }
21236
21723
  function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
21237
- rmSync20(endpointFile(stateDir, ticketRecordId), { force: true });
21724
+ rmSync22(endpointFile(stateDir, ticketRecordId), { force: true });
21238
21725
  }
21239
21726
  function listPeriscopeEndpoints(stateDir) {
21240
21727
  const dir = endpointDir(stateDir);
@@ -21331,7 +21818,7 @@ async function realAncestor(p) {
21331
21818
  }
21332
21819
  const parent = dirname24(cur);
21333
21820
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
21334
- tail3.push(basename13(cur));
21821
+ tail3.push(basename14(cur));
21335
21822
  cur = parent;
21336
21823
  }
21337
21824
  }
@@ -21341,13 +21828,13 @@ async function resolveInWorktree(root, requested) {
21341
21828
  let abs;
21342
21829
  if (isAbsolute3(requested)) {
21343
21830
  abs = resolve5(requested);
21344
- if (isInside(root, abs)) abs = join46(realRoot, relative2(root, abs));
21831
+ if (isInside(root, abs)) abs = join48(realRoot, relative2(root, abs));
21345
21832
  } else {
21346
21833
  abs = resolve5(realRoot, requested);
21347
21834
  }
21348
21835
  if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21349
21836
  const { real, rest } = await realAncestor(abs);
21350
- const final = rest ? join46(real, rest) : real;
21837
+ const final = rest ? join48(real, rest) : real;
21351
21838
  if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21352
21839
  if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
21353
21840
  return final;
@@ -21415,7 +21902,7 @@ function createPeriscopeMcpServer(d, sc) {
21415
21902
  const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
21416
21903
  const lines2 = await Promise.all(entries.map(async (e) => {
21417
21904
  if (e.isDirectory()) return `[DIR] ${e.name}`;
21418
- const size = (await lstat(join46(abs, e.name))).size;
21905
+ const size = (await lstat(join48(abs, e.name))).size;
21419
21906
  return `[FILE] ${e.name} (${size} bytes)`;
21420
21907
  }));
21421
21908
  return text(lines2.join("\n") || "(empty)");
@@ -21481,8 +21968,8 @@ function createPeriscopeMcpServer(d, sc) {
21481
21968
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
21482
21969
  }, async ({ path }) => {
21483
21970
  const parent = await inTree(dirname24(path) === "" ? "." : dirname24(path));
21484
- const abs = join46(parent, basename13(path));
21485
- if (basename13(path) === ".git" || abs === parent) return text("path must name a file", true);
21971
+ const abs = join48(parent, basename14(path));
21972
+ if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
21486
21973
  const st = await lstat(abs).catch(() => void 0);
21487
21974
  if (!st) return text(`${path} does not exist`, true);
21488
21975
  if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
@@ -21888,6 +22375,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
21888
22375
  "rotate-passenger-url",
21889
22376
  "run",
21890
22377
  "sample-ticket",
22378
+ "sandbox",
21891
22379
  "selftest",
21892
22380
  "shell-init",
21893
22381
  "ship",
@@ -21967,7 +22455,7 @@ if (command === "provision") {
21967
22455
  process.exit(outcome.code);
21968
22456
  }
21969
22457
  if (command === "pool-member") {
21970
- const pool = poolConfigFrom(argv, process.env, () => readFileSync39(0, "utf8"));
22458
+ const pool = poolConfigFrom(argv, process.env, () => readFileSync40(0, "utf8"));
21971
22459
  if (typeof pool === "string") {
21972
22460
  process.stderr.write(`crew pool-member: ${pool}
21973
22461
  `);
@@ -22008,9 +22496,9 @@ if (command === "passenger-sync-daemon") {
22008
22496
  return m ? m[1] : null;
22009
22497
  }, routeRelayUrl = function(route3) {
22010
22498
  const ws = route3.resolved?.workspaceId;
22011
- const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
22012
- if (!cfg.ship.relayHost || !slug) return null;
22013
- 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);
22014
22502
  }, passengerApiKeys = function(routes) {
22015
22503
  const keys = /* @__PURE__ */ new Map();
22016
22504
  for (const r of routes) {
@@ -22075,8 +22563,8 @@ if (command === "passenger-sync-daemon") {
22075
22563
  stateDir: cfg.ship.stateDir,
22076
22564
  shipName: cfg.ship.name,
22077
22565
  apiFor: endpointApiFor(routes),
22078
- urlFor: (slug) => publicUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
22079
- 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`,
22080
22568
  warn: (m) => emit.warn(m, { step: "passengers" })
22081
22569
  };
22082
22570
  }, releaseRefused = function() {
@@ -22096,8 +22584,20 @@ if (command === "passenger-sync-daemon") {
22096
22584
  const out = refreshBaseBranch(r.dir, remote, base, true);
22097
22585
  const unsafe = baseBranchUnsafe(out);
22098
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
+ };
22099
22599
  };
22100
- 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;
22101
22601
  let cfg;
22102
22602
  let firstRun;
22103
22603
  let firstRunFromFlags = false;
@@ -22168,7 +22668,7 @@ if (command === "passenger-sync-daemon") {
22168
22668
  throw e;
22169
22669
  }
22170
22670
  if (command === "credential") {
22171
- 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);
22172
22672
  process.stdout.write(r.out);
22173
22673
  process.stderr.write(r.err);
22174
22674
  process.exit(r.code);
@@ -22180,9 +22680,9 @@ if (command === "passenger-sync-daemon") {
22180
22680
  forges: cfg.ship.forges,
22181
22681
  stateDir: cfg.ship.stateDir,
22182
22682
  configFile: cfg.configFile,
22183
- readFile: (p) => readFileSync39(p, "utf8"),
22184
- writeFile: (p, t) => writeFileSync31(p, t),
22185
- stdin: () => readFileSync39(0, "utf8"),
22683
+ readFile: (p) => readFileSync40(p, "utf8"),
22684
+ writeFile: (p, t) => writeFileSync33(p, t),
22685
+ stdin: () => readFileSync40(0, "utf8"),
22186
22686
  env: process.env
22187
22687
  }
22188
22688
  );
@@ -22202,7 +22702,7 @@ if (command === "passenger-sync-daemon") {
22202
22702
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
22203
22703
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
22204
22704
  const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
22205
- 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;
22206
22706
  let route2;
22207
22707
  try {
22208
22708
  route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -22465,7 +22965,7 @@ if (command === "passenger-sync-daemon") {
22465
22965
  }
22466
22966
  if (host === "linux" && hasSystemd) {
22467
22967
  const label = labelFor(CREW_HOME, "run");
22468
- const staleTimer = join47(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22968
+ const staleTimer = join49(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22469
22969
  if (existsSync45(staleTimer)) {
22470
22970
  process.stdout.write(
22471
22971
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -22528,7 +23028,7 @@ if (command === "passenger-sync-daemon") {
22528
23028
  for (const a of plan.actions) {
22529
23029
  if (a.kind === "enable") {
22530
23030
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
22531
- writeFileSync31(cfg.configFile, planEnable(readFileSync39(cfg.configFile, "utf8"), a.route));
23031
+ writeFileSync33(cfg.configFile, planEnable(readFileSync40(cfg.configFile, "utf8"), a.route));
22532
23032
  enabledSound.add(a.route);
22533
23033
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
22534
23034
  } else {
@@ -22547,6 +23047,14 @@ ${changed2.map((c) => ` - ${c}
22547
23047
  async function resolvedRepos(c) {
22548
23048
  return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
22549
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
+ }
22550
23058
  async function resolveOneRepo(c, t) {
22551
23059
  {
22552
23060
  const id = repoIdForName(c, t.name);
@@ -23651,7 +24159,7 @@ ${cu.tail.trimEnd()}
23651
24159
  process.exit(1);
23652
24160
  }
23653
24161
  requireArmed("run the daemon loop");
23654
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join47(CREW_HOME, "src"), join47(CREW_HOME, "dist"), join47(CREW_HOME, "bin")];
24162
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join49(CREW_HOME, "src"), join49(CREW_HOME, "dist"), join49(CREW_HOME, "bin")];
23655
24163
  const configPaths = daemonConfigPaths(cfg);
23656
24164
  watchPaths.push(...configPaths);
23657
24165
  noteDaemonStarted(cfg.ship.stateDir);
@@ -23717,6 +24225,7 @@ ${cu.tail.trimEnd()}
23717
24225
  });
23718
24226
  if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
23719
24227
  const inFlight = /* @__PURE__ */ new Set();
24228
+ let sandboxBuilding = false;
23720
24229
  const stopReason = await runDaemonLoop({
23721
24230
  inFlight,
23722
24231
  onStaleDeferred: ({ runs, waitedMs }) => emit.warn(
@@ -23762,6 +24271,12 @@ ${cu.tail.trimEnd()}
23762
24271
  ),
23763
24272
  checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
23764
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
+ }
23765
24280
  if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
23766
24281
  for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
23767
24282
  }
@@ -23864,9 +24379,9 @@ ${cu.tail.trimEnd()}
23864
24379
  `);
23865
24380
  process.exit(2);
23866
24381
  }
23867
- let run2;
24382
+ let run3;
23868
24383
  try {
23869
- run2 = await runAgentsSync(target, {
24384
+ run3 = await runAgentsSync(target, {
23870
24385
  userAgent: cfg.ship.userAgent,
23871
24386
  stateDir: cfg.ship.stateDir,
23872
24387
  dryRun,
@@ -23884,7 +24399,7 @@ ${cu.tail.trimEnd()}
23884
24399
  }
23885
24400
  throw e;
23886
24401
  }
23887
- const { personaResult, skillResult } = run2;
24402
+ const { personaResult, skillResult } = run3;
23888
24403
  if (flag("revoke-seat-keys")) {
23889
24404
  for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
23890
24405
  process.stdout.write(`${describeRevokeOutcome(o)}
@@ -23905,7 +24420,7 @@ ${cu.tail.trimEnd()}
23905
24420
  }
23906
24421
  }
23907
24422
  }
23908
- if (run2.diverged) process.exitCode = 1;
24423
+ if (run3.diverged) process.exitCode = 1;
23909
24424
  break;
23910
24425
  }
23911
24426
  case "skills": {
@@ -23947,10 +24462,10 @@ ${cu.tail.trimEnd()}
23947
24462
  `);
23948
24463
  if (!dryRun) {
23949
24464
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
23950
- const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync39(resolvedPath, "utf8")) : {};
24465
+ const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync40(resolvedPath, "utf8")) : {};
23951
24466
  raw.agentSkills = result.agentSkills;
23952
- mkdirSync29(dirOf2(resolvedPath), { recursive: true });
23953
- writeFileSync31(resolvedPath, `${JSON.stringify(raw, null, 2)}
24467
+ mkdirSync30(dirOf2(resolvedPath), { recursive: true });
24468
+ writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
23954
24469
  `);
23955
24470
  }
23956
24471
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -23973,7 +24488,7 @@ ${cu.tail.trimEnd()}
23973
24488
  throw e;
23974
24489
  }
23975
24490
  const oldName = cfg.ship.name;
23976
- writeFileSync31(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), newName));
24491
+ writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), newName));
23977
24492
  cfg.ship.name = newName;
23978
24493
  let renameMachineId;
23979
24494
  try {
@@ -24020,7 +24535,7 @@ ${cu.tail.trimEnd()}
24020
24535
  process.exit(2);
24021
24536
  };
24022
24537
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
24023
- const configText = readFileSync39(cfg.configFile, "utf8");
24538
+ const configText = readFileSync40(cfg.configFile, "utf8");
24024
24539
  if (sub === "check") {
24025
24540
  try {
24026
24541
  const routeArg = value("route") ?? positional[2];
@@ -24060,7 +24575,7 @@ ${cu.tail.trimEnd()}
24060
24575
  }
24061
24576
  }, read: (r) => {
24062
24577
  try {
24063
- return readFileSync39(resolvePath(dir, r), "utf8");
24578
+ return readFileSync40(resolvePath(dir, r), "utf8");
24064
24579
  } catch {
24065
24580
  return void 0;
24066
24581
  }
@@ -24102,19 +24617,19 @@ ${cu.tail.trimEnd()}
24102
24617
  let cleared;
24103
24618
  if (clear2) {
24104
24619
  const plan = clearRouteHooks(configText, a.route, a.name);
24105
- if (plan.removed.length > 0) writeFileSync31(cfg.configFile, plan.text);
24620
+ if (plan.removed.length > 0) writeFileSync33(cfg.configFile, plan.text);
24106
24621
  cleared = { file: cfg.configFile, removed: plan.removed };
24107
24622
  }
24108
24623
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
24109
24624
  if (write === "route") {
24110
24625
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
24111
- writeFileSync31(cfg.configFile, plan.text);
24626
+ writeFileSync33(cfg.configFile, plan.text);
24112
24627
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
24113
24628
  } else if (write === "repo") {
24114
24629
  const have = existingRepoConfig(files);
24115
24630
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
24116
24631
  const file = resolvePath(dir, ".crew.yaml");
24117
- writeFileSync31(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24632
+ writeFileSync33(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24118
24633
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
24119
24634
  }
24120
24635
  if (flag("json")) {
@@ -24211,7 +24726,7 @@ ${cu.tail.trimEnd()}
24211
24726
  let resolvedRemotes;
24212
24727
  if (existsSync45(resolvedFile)) {
24213
24728
  try {
24214
- resolvedRemotes = JSON.parse(readFileSync39(resolvedFile, "utf8"));
24729
+ resolvedRemotes = JSON.parse(readFileSync40(resolvedFile, "utf8"));
24215
24730
  } catch {
24216
24731
  }
24217
24732
  }
@@ -24232,7 +24747,7 @@ ${cu.tail.trimEnd()}
24232
24747
  isCheckout,
24233
24748
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
24234
24749
  try {
24235
- 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;
24236
24751
  } catch {
24237
24752
  return false;
24238
24753
  }
@@ -24281,7 +24796,7 @@ ${cu.tail.trimEnd()}
24281
24796
  throw new ReposError(`${e.message}${authHint}
24282
24797
  crew.yaml was not changed`);
24283
24798
  }
24284
- writeFileSync31(cfg.configFile, plan2.text);
24799
+ writeFileSync33(cfg.configFile, plan2.text);
24285
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:
24286
24801
  `);
24287
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}`);
@@ -24341,14 +24856,14 @@ ${cu.tail.trimEnd()}
24341
24856
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
24342
24857
  `);
24343
24858
  if (existsSync45(resolvedFile)) {
24344
- const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
24859
+ const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
24345
24860
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
24346
24861
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
24347
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
24862
+ writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
24348
24863
  `);
24349
24864
  }
24350
24865
  }
24351
- writeFileSync31(cfg.configFile, plan.text);
24866
+ writeFileSync33(cfg.configFile, plan.text);
24352
24867
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
24353
24868
  `);
24354
24869
  } catch (e) {
@@ -24717,7 +25232,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24717
25232
  process.exit(2);
24718
25233
  }
24719
25234
  if (found.shipNameAdopted && !dryRun) {
24720
- writeFileSync31(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), found.shipNameAdopted));
25235
+ writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), found.shipNameAdopted));
24721
25236
  cfg.ship.name = found.shipNameAdopted;
24722
25237
  }
24723
25238
  if (found.shipRow) {
@@ -24763,7 +25278,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24763
25278
  const hadPreviousFile = existsSync45(resolvedPath);
24764
25279
  if (hadPreviousFile) {
24765
25280
  try {
24766
- previous = JSON.parse(readFileSync39(resolvedPath, "utf8"));
25281
+ previous = JSON.parse(readFileSync40(resolvedPath, "utf8"));
24767
25282
  } catch {
24768
25283
  }
24769
25284
  }
@@ -24893,12 +25408,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24893
25408
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
24894
25409
  `);
24895
25410
  } else {
24896
- mkdirSync29(dirname25(resolvedPath), { recursive: true });
25411
+ mkdirSync30(dirname25(resolvedPath), { recursive: true });
24897
25412
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
24898
25413
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
24899
- writeFileSync31(tmpPath, `${JSON.stringify(resolved, null, 2)}
25414
+ writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
24900
25415
  `);
24901
- renameSync5(tmpPath, resolvedPath);
25416
+ renameSync6(tmpPath, resolvedPath);
24902
25417
  process.stderr.write(`Resolved ids written to ${resolvedPath}
24903
25418
  `);
24904
25419
  }
@@ -24942,8 +25457,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24942
25457
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
24943
25458
  `);
24944
25459
  } else {
24945
- mkdirSync29(dirname25(apiKeyFile), { recursive: true });
24946
- writeFileSync31(apiKeyFile, `${apiKeyVar}=${apiKey}
25460
+ mkdirSync30(dirname25(apiKeyFile), { recursive: true });
25461
+ writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
24947
25462
  `, { mode: 384 });
24948
25463
  process.stderr.write(`API key written to ${apiKeyFile}
24949
25464
  `);
@@ -25076,7 +25591,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25076
25591
  if (syncRoute?.resolved) {
25077
25592
  try {
25078
25593
  process.stderr.write("\n");
25079
- const run2 = await runAgentsSync(syncRoute, {
25594
+ const run3 = await runAgentsSync(syncRoute, {
25080
25595
  userAgent: cfg.ship.userAgent,
25081
25596
  stateDir: cfg.ship.stateDir,
25082
25597
  say: (l) => process.stderr.write(` ${l}
@@ -25091,19 +25606,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25091
25606
  if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role, p.tables)}
25092
25607
  `);
25093
25608
  }
25094
- if (run2.readOnly) {
25095
- 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");
25096
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");
25097
25612
  }
25098
- 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", {
25099
25614
  route: route3,
25100
- admin: !run2.readOnly,
25101
- personas: run2.personaResult.outcomes,
25102
- skills: run2.skillResult?.outcomes ?? [],
25103
- diverged: run2.diverged
25615
+ admin: !run3.readOnly,
25616
+ personas: run3.personaResult.outcomes,
25617
+ skills: run3.skillResult?.outcomes ?? [],
25618
+ diverged: run3.diverged
25104
25619
  });
25105
25620
  for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route3, ...m });
25106
- if (run2.diverged) process.exitCode = 1;
25621
+ if (run3.diverged) process.exitCode = 1;
25107
25622
  } catch (e) {
25108
25623
  if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
25109
25624
  else throw e;
@@ -25491,6 +26006,70 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25491
26006
  if (!planIsUsable(planWt)) process.exitCode = 1;
25492
26007
  break;
25493
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
+ }
25494
26073
  case "periscope": {
25495
26074
  const subP = positional[1];
25496
26075
  if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
@@ -25773,10 +26352,10 @@ ${where.join("\n")}`);
25773
26352
  },
25774
26353
  prepare: async (path) => {
25775
26354
  for (const f of repoP.config.worktrees.copy) {
25776
- const src = join47(repoP.dir, f);
26355
+ const src = join49(repoP.dir, f);
25777
26356
  if (!existsSync45(src)) continue;
25778
- mkdirSync29(dirOf2(join47(path, f)), { recursive: true });
25779
- copyFileSync2(src, join47(path, f));
26357
+ mkdirSync30(dirOf2(join49(path, f)), { recursive: true });
26358
+ copyFileSync2(src, join49(path, f));
25780
26359
  }
25781
26360
  const setup = repoP.config.hooks.setup;
25782
26361
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -26050,9 +26629,9 @@ ${where.join("\n")}`);
26050
26629
  } };
26051
26630
  const store = resetStore;
26052
26631
  const h = hostFromUrl4(r.baseUrl);
26053
- const [slug] = splitRoute(r.route);
26054
- const session = await store.get(h, slug);
26055
- 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) };
26056
26635
  },
26057
26636
  async hostSessions(stateDir) {
26058
26637
  const store = resetStore;
@@ -26071,7 +26650,7 @@ ${where.join("\n")}`);
26071
26650
  };
26072
26651
  const droppedRoutes = [];
26073
26652
  try {
26074
- const rawCfg = parseYaml2(readFileSync39(cfg.configFile, "utf8"));
26653
+ const rawCfg = parseYaml2(readFileSync40(cfg.configFile, "utf8"));
26075
26654
  for (const r of rawCfg?.routes ?? []) {
26076
26655
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
26077
26656
  try {
@@ -26124,8 +26703,8 @@ ${where.join("\n")}`);
26124
26703
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
26125
26704
  break;
26126
26705
  }
26127
- mkdirSync29(cfg.ship.stateDir, { recursive: true });
26128
- writeFileSync31(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26706
+ mkdirSync30(cfg.ship.stateDir, { recursive: true });
26707
+ writeFileSync33(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26129
26708
  `);
26130
26709
  process.stdout.write(
26131
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"
@@ -26178,7 +26757,7 @@ ${where.join("\n")}`);
26178
26757
  }
26179
26758
  case "log": {
26180
26759
  try {
26181
- const text2 = readFileSync39(cfg.ship.logFile, "utf8").split("\n");
26760
+ const text2 = readFileSync40(cfg.ship.logFile, "utf8").split("\n");
26182
26761
  process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
26183
26762
  `);
26184
26763
  } catch {
@@ -26201,7 +26780,7 @@ ${where.join("\n")}`);
26201
26780
  };
26202
26781
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
26203
26782
  const tracker = new Tracker(route2, cfg.ship);
26204
- const configText = readFileSync39(cfg.configFile, "utf8");
26783
+ const configText = readFileSync40(cfg.configFile, "utf8");
26205
26784
  const cfgBase = dirname25(cfg.configFile);
26206
26785
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
26207
26786
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
@@ -26230,7 +26809,7 @@ ${where.join("\n")}`);
26230
26809
  }
26231
26810
  if (configured2.some((r) => r.dir === dir)) {
26232
26811
  try {
26233
- writeFileSync31(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26812
+ writeFileSync33(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26234
26813
  } catch (e) {
26235
26814
  if (e instanceof ReposError) fail(e.message);
26236
26815
  throw e;
@@ -26239,28 +26818,28 @@ ${where.join("\n")}`);
26239
26818
  if (row) {
26240
26819
  await tracker.deleteRepoRow(row.id);
26241
26820
  if (existsSync45(resolvedFile)) {
26242
- const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26821
+ const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
26243
26822
  if (raw.repoNames) delete raw.repoNames[row.id];
26244
26823
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
26245
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
26824
+ writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
26246
26825
  `);
26247
26826
  }
26248
26827
  }
26249
- if (existsSync45(join47(dir, ".git")) && basename14(dir) === SAMPLE_REPO) rmSync21(dir, { recursive: true, force: true });
26828
+ if (existsSync45(join49(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
26250
26829
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
26251
26830
  break;
26252
26831
  }
26253
26832
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
26254
- const fresh = !existsSync45(join47(dir, ".git"));
26833
+ const fresh = !existsSync45(join49(dir, ".git"));
26255
26834
  if (existsSync45(dir) && fresh && readdirSync20(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
26256
26835
  if (dryRun) {
26257
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`);
26258
26837
  break;
26259
26838
  }
26260
26839
  if (fresh) {
26261
- mkdirSync29(dir, { recursive: true });
26262
- writeFileSync31(join47(dir, "README.md"), SAMPLE_README);
26263
- writeFileSync31(join47(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);
26264
26843
  const g = (...a) => {
26265
26844
  const r = spawnSync10("git", a, { cwd: dir, encoding: "utf8" });
26266
26845
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -26279,16 +26858,16 @@ ${where.join("\n")}`);
26279
26858
  });
26280
26859
  repoId = (await tracker.createRepoRow(payload)).id;
26281
26860
  if (existsSync45(resolvedFile)) {
26282
- const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26861
+ const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
26283
26862
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
26284
26863
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
26285
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
26864
+ writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
26286
26865
  `);
26287
26866
  }
26288
26867
  }
26289
26868
  if (!configured2.some((r) => r.dir === dir)) {
26290
26869
  try {
26291
- writeFileSync31(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);
26292
26871
  } catch (e) {
26293
26872
  if (e instanceof ReposError) fail(e.message);
26294
26873
  throw e;
@@ -26523,7 +27102,7 @@ ${where.join("\n")}`);
26523
27102
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
26524
27103
  // config-provenance display; this is the field that actually
26525
27104
  // describes where a worktree lands by default.
26526
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
27105
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
26527
27106
  worktreeParent: resolvePath(r.dir, "..")
26528
27107
  })),
26529
27108
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -26731,7 +27310,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
26731
27310
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
26732
27311
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
26733
27312
  try {
26734
- return readFileSync39(p, "utf8");
27313
+ return readFileSync40(p, "utf8");
26735
27314
  } catch {
26736
27315
  return null;
26737
27316
  }
@@ -26781,7 +27360,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
26781
27360
  );
26782
27361
  for (const r of await resolvedRepos(route2)) {
26783
27362
  const { config } = r;
26784
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename14(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
27363
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
26785
27364
  process.stdout.write(
26786
27365
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
26787
27366
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -27103,6 +27682,13 @@ ${foreign.join("\n")}
27103
27682
  `);
27104
27683
  }
27105
27684
  }
27685
+ if (cfg.ship.runner === "sandbox") {
27686
+ const sb = checkSandboxRunner(cfg.ship.sandbox);
27687
+ process.stdout.write(`sandbox runner: ${sb.ok ? `OK (${sb.detail})` : `NOT READY \u2014 ${sb.detail}`}
27688
+ `);
27689
+ } else {
27690
+ process.stdout.write("sandbox runner: off (ship.runner is host)\n");
27691
+ }
27106
27692
  const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
27107
27693
  if (passengerRoutes.length === 0) {
27108
27694
  process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
@@ -27146,13 +27732,13 @@ ${foreign.join("\n")}
27146
27732
  if (cfg.ship.relayHost) {
27147
27733
  for (const r of passengerRoutes) {
27148
27734
  const ws = r.resolved?.workspaceId;
27149
- const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
27735
+ const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
27150
27736
  if (!ws) continue;
27151
27737
  process.stdout.write(
27152
- ` ${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)"}
27153
27739
  `
27154
27740
  );
27155
- 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;
27156
27742
  process.stdout.write(` ${r.route} control url: ${controlUrl ?? "none (the tunnel carries passengers only)"}
27157
27743
  `);
27158
27744
  const api = endpointApiFor([r])(ws);
@@ -27171,6 +27757,15 @@ ${foreign.join("\n")}
27171
27757
  );
27172
27758
  }
27173
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
+ }
27174
27769
  for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
27175
27770
  const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
27176
27771
  process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret}, attached only)
@@ -27229,3 +27824,4 @@ var endpointReconcileDeps2;
27229
27824
  var releaseRefused2;
27230
27825
  var doctorMachineId2;
27231
27826
  var baseHealth2;
27827
+ var sandboxTickDeps2;