@tablation/crew 0.103.0 → 0.104.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 +550 -310
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2678,6 +2678,23 @@ function parseCloneScheme(v) {
2678
2678
  if (v === "ssh" || v === "https") return v;
2679
2679
  throw new ConfigError(`ship.cloneScheme must be "ssh" or "https", got ${JSON.stringify(v)}`);
2680
2680
  }
2681
+ function parseShipRunner(v) {
2682
+ if (v === void 0 || v === null || v === "") return void 0;
2683
+ if (v === "host" || v === "sandbox") return v;
2684
+ throw new ConfigError(`ship.runner must be "host" or "sandbox", got ${JSON.stringify(v)}`);
2685
+ }
2686
+ var SANDBOX_KEYS = ["bin", "image", "policy", "cacheVolume", "mirrorDir", "workDir", "agentBin", "forgeTokenVar"];
2687
+ function parseShipSandbox(v) {
2688
+ if (v === void 0 || v === null) return void 0;
2689
+ if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.sandbox must be a map of settings");
2690
+ const out = {};
2691
+ for (const [k, val] of Object.entries(v)) {
2692
+ if (!SANDBOX_KEYS.includes(k)) throw new ConfigError(`ship.sandbox: unknown setting "${k}" (known: ${SANDBOX_KEYS.join(", ")})`);
2693
+ if (typeof val !== "string" || !val) throw new ConfigError(`ship.sandbox.${k} must be a non-empty string`);
2694
+ out[k] = val;
2695
+ }
2696
+ return out;
2697
+ }
2681
2698
  function parseShipEnv(v) {
2682
2699
  if (v === void 0 || v === null) return void 0;
2683
2700
  if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.env must be a map of variable name to value");
@@ -3124,6 +3141,8 @@ Create the first of those:
3124
3141
  if (dupe) missing.add(`routes: duplicate route "${dupe}" \u2014 a route addresses itself, so it must be unique`);
3125
3142
  missing.throwIfAny(file);
3126
3143
  configureSecretStore({ mode: secretStore, stateDir });
3144
+ const shipRunner = parseShipRunner(shipRaw.runner);
3145
+ const shipSandbox = parseShipSandbox(shipRaw.sandbox);
3127
3146
  return {
3128
3147
  ship: {
3129
3148
  name: shipRaw.name ?? "this ship",
@@ -3133,6 +3152,7 @@ Create the first of those:
3133
3152
  model: shipRaw.agent?.model ?? "claude-sonnet-5",
3134
3153
  maxThinkingTokens
3135
3154
  },
3155
+ ...shipRunner ? { runner: shipRunner, ...shipSandbox ? { sandbox: shipSandbox } : {} } : {},
3136
3156
  ...toolchains.length > 0 ? { toolchains } : {},
3137
3157
  ...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
3138
3158
  ...pair ? { pair } : {},
@@ -5418,17 +5438,17 @@ function roleCapWarnings(ship) {
5418
5438
  }
5419
5439
 
5420
5440
  // 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";
5441
+ import { spawn as spawn4 } from "child_process";
5442
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, readFileSync as readFileSync20, rmSync as rmSync13, writeFileSync as writeFileSync15 } from "fs";
5443
+ import { tmpdir as tmpdir3 } from "os";
5444
+ import { join as join24 } from "path";
5425
5445
 
5426
5446
  // 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";
5447
+ import { spawnSync as spawnSync5 } from "child_process";
5448
+ import { readFileSync as readFileSync19, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync12, writeFileSync as writeFileSync14, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5429
5449
  import { StringDecoder } from "string_decoder";
5430
5450
  import { finished } from "stream/promises";
5431
- import { join as join22, dirname as dirname8, basename as basename7 } from "path";
5451
+ import { join as join23, dirname as dirname8, basename as basename8 } from "path";
5432
5452
  import { createHash as createHash4 } from "crypto";
5433
5453
 
5434
5454
  // src/state.ts
@@ -9457,6 +9477,13 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
9457
9477
  return out;
9458
9478
  }
9459
9479
 
9480
+ // src/runner.ts
9481
+ import { spawn as spawn2, execFile, execFileSync as execFileSync5 } from "child_process";
9482
+ import { mkdtempSync, writeFileSync as writeFileSync13, rmSync as rmSync12 } from "fs";
9483
+ import { tmpdir as tmpdir2 } from "os";
9484
+ import { basename as basename7, join as join22 } from "path";
9485
+ import { promisify } from "util";
9486
+
9460
9487
  // src/run-limit.ts
9461
9488
  var DEFAULT_MAX_RUN_MINUTES = 240;
9462
9489
  var KILL_GRACE_MS = 5e3;
@@ -9506,6 +9533,186 @@ function runKilledComment(b) {
9506
9533
  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
9534
  }
9508
9535
 
9536
+ // src/runner.ts
9537
+ var execFileP = promisify(execFile);
9538
+ var DEFAULT_SANDBOX_CONFIG = {
9539
+ bin: "openshell",
9540
+ mirrorDir: "/cache",
9541
+ workDir: "/sandbox/work"
9542
+ };
9543
+ var SANDBOX_STDIN_LIMIT = 4 * 1024 * 1024 - 64 * 1024;
9544
+ var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
9545
+ var HostRunner = class {
9546
+ kind = "host";
9547
+ start(plan, o) {
9548
+ const child = spawn2(plan.bin, plan.args, {
9549
+ cwd: plan.cwd,
9550
+ env: o.env,
9551
+ stdio: ["pipe", o.streamFd, "inherit"],
9552
+ detached: true
9553
+ });
9554
+ return { child, handle: { runner: "host", pid: child.pid } };
9555
+ }
9556
+ async kill(handle) {
9557
+ if (handle.pid !== void 0) await killRunGroup(handle.pid);
9558
+ }
9559
+ isAlive(handle) {
9560
+ if (handle.pid === void 0) return false;
9561
+ try {
9562
+ process.kill(handle.pid, 0);
9563
+ return true;
9564
+ } catch (e) {
9565
+ return e.code === "EPERM";
9566
+ }
9567
+ }
9568
+ reattach(meta) {
9569
+ return { runner: "host", pid: meta.pid };
9570
+ }
9571
+ async release() {
9572
+ }
9573
+ };
9574
+ function shQuote2(s) {
9575
+ return `'${s.replace(/'/g, `'\\''`)}'`;
9576
+ }
9577
+ function sandboxName(plan, now = Date.now()) {
9578
+ const base = [plan.route, plan.role, plan.ticket ?? ""].join("-").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9579
+ return `crew-${base.slice(0, 36)}-${now.toString(36)}`.replace(/-+/g, "-");
9580
+ }
9581
+ var SandboxRunner = class {
9582
+ kind = "sandbox";
9583
+ cfg;
9584
+ /** Injected for tests; the real one reads the plan's checkout. */
9585
+ originOf;
9586
+ constructor(cfg = {}, originOf = gitOrigin) {
9587
+ this.cfg = { ...DEFAULT_SANDBOX_CONFIG, ...cfg };
9588
+ this.originOf = originOf;
9589
+ }
9590
+ os(args, o = {}) {
9591
+ return execFileP(this.cfg.bin, args, { env: o.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
9592
+ }
9593
+ async start(plan, o) {
9594
+ const origin = this.originOf(plan.cwd);
9595
+ if (!origin) throw new Error(`cannot find the origin remote of ${plan.cwd}, so there is nothing to clone into the sandbox`);
9596
+ const repoName = basename7(origin).replace(/\.git$/, "");
9597
+ const name = sandboxName(plan);
9598
+ const { cfg } = this;
9599
+ const secretKeys = Object.keys(plan.setEnv);
9600
+ if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0 && !secretKeys.includes(cfg.forgeTokenVar)) secretKeys.push(cfg.forgeTokenVar);
9601
+ const createEnv = { ...process.env, ...plan.setEnv };
9602
+ if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0) createEnv[cfg.forgeTokenVar] = o.env[cfg.forgeTokenVar];
9603
+ const create2 = ["sandbox", "create", "--name", name, "--detach"];
9604
+ if (cfg.policy) create2.push("--policy", cfg.policy);
9605
+ if (cfg.image) create2.push("--from", cfg.image);
9606
+ if (cfg.cacheVolume) {
9607
+ create2.push("--driver-config-json", JSON.stringify({
9608
+ docker: { mounts: [{ type: "volume", source: cfg.cacheVolume, target: cfg.mirrorDir, read_only: true }] }
9609
+ }));
9610
+ }
9611
+ for (const k of secretKeys) create2.push("--env-from", k);
9612
+ const maxSeconds = Math.max(60, Math.ceil((plan.maxRunMinutes ?? 0) * 60) + 600);
9613
+ create2.push("--", "sleep", String(plan.maxRunMinutes ? maxSeconds : 86400));
9614
+ const t0 = Date.now();
9615
+ try {
9616
+ await this.os(create2, { env: createEnv });
9617
+ } catch (e) {
9618
+ await this.deleteQuietly(name);
9619
+ throw new Error(`openshell could not create the sandbox: ${describeExecError(e)}`);
9620
+ }
9621
+ o.emit.emit(`sandbox_created: ${name}`, { role: plan.role, data: { event: "sandbox_created", sandbox: name, ms: Date.now() - t0 } });
9622
+ const handle = { runner: "sandbox", sandbox: name, sandboxBin: cfg.bin };
9623
+ try {
9624
+ const branch = `crew-run-${name.slice(-8)}`;
9625
+ const clone = [
9626
+ `git clone --quiet --shared ${shQuote2(`${cfg.mirrorDir}/${repoName}.git`)} ${shQuote2(cfg.workDir)}`,
9627
+ `cd ${shQuote2(cfg.workDir)}`,
9628
+ `git remote set-url origin ${shQuote2(origin)}`,
9629
+ `git checkout --quiet -b ${shQuote2(branch)}`
9630
+ ].join(" && ");
9631
+ await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
9632
+ const harness = cfg.agentBin ?? basename7(plan.bin);
9633
+ const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
9634
+ if (viaUpload) {
9635
+ const tmp = mkdtempSync(join22(tmpdir2(), "crew-prompt-"));
9636
+ try {
9637
+ const file = join22(tmp, "prompt.txt");
9638
+ writeFileSync13(file, plan.prompt, { mode: 384 });
9639
+ await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
9640
+ } finally {
9641
+ rmSync12(tmp, { recursive: true, force: true });
9642
+ }
9643
+ }
9644
+ const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
9645
+ const child = spawn2(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
9646
+ env: process.env,
9647
+ stdio: ["pipe", o.streamFd, "inherit"],
9648
+ detached: true
9649
+ });
9650
+ return { child, handle: { ...handle, pid: child.pid }, ...viaUpload ? { promptDelivered: true } : {} };
9651
+ } catch (e) {
9652
+ await this.release(handle, o.emit);
9653
+ throw new Error(`could not prepare the sandbox ${name}: ${describeExecError(e)}`);
9654
+ }
9655
+ }
9656
+ async kill(handle) {
9657
+ if (handle.sandbox) await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
9658
+ if (handle.pid !== void 0) await killRunGroup(handle.pid, { graceMs: 2e3 });
9659
+ }
9660
+ isAlive(handle) {
9661
+ return new HostRunner().isAlive(handle);
9662
+ }
9663
+ reattach(meta) {
9664
+ return { runner: "sandbox", pid: meta.pid, sandbox: meta.sandbox, sandboxBin: meta.sandboxBin };
9665
+ }
9666
+ async release(handle, emit) {
9667
+ if (!handle.sandbox) return;
9668
+ const t0 = Date.now();
9669
+ await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
9670
+ emit.emit(`sandbox_deleted: ${handle.sandbox}`, { data: { event: "sandbox_deleted", sandbox: handle.sandbox, ms: Date.now() - t0 } });
9671
+ }
9672
+ async deleteQuietly(name, bin = this.cfg.bin) {
9673
+ try {
9674
+ await execFileP(bin, ["sandbox", "delete", name], { timeout: 6e4 });
9675
+ } catch {
9676
+ }
9677
+ }
9678
+ };
9679
+ function describeExecError(e) {
9680
+ const err3 = e;
9681
+ return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
9682
+ }
9683
+ function gitOrigin(cwd) {
9684
+ try {
9685
+ return execFileSync5("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9686
+ } catch {
9687
+ return null;
9688
+ }
9689
+ }
9690
+ function runnerFor(kind, sandbox) {
9691
+ return kind === "sandbox" ? new SandboxRunner(sandbox) : new HostRunner();
9692
+ }
9693
+ function runnerOfSidecar(sc) {
9694
+ return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
9695
+ }
9696
+ function checkSandboxRunner(cfg = {}, run2 = (b, a) => execFileSync5(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
9697
+ const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
9698
+ let version;
9699
+ try {
9700
+ version = run2(bin, ["--version"]).trim();
9701
+ } catch (e) {
9702
+ return { ok: false, detail: `${bin} is not installed or not on the scheduler PATH (${e.message.split("\n")[0]})` };
9703
+ }
9704
+ try {
9705
+ run2(bin, ["status"]);
9706
+ } catch (e) {
9707
+ const err3 = e;
9708
+ return {
9709
+ ok: false,
9710
+ 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.`
9711
+ };
9712
+ }
9713
+ return { ok: true, detail: version };
9714
+ }
9715
+
9509
9716
  // src/agent.ts
9510
9717
  var DIGEST_MAX_AGE_SECONDS = 600;
9511
9718
  var LEGACY_TOOLCHAIN_HARNESS = "claude";
@@ -9556,13 +9763,13 @@ function streamShowsAllowanceRefusal(streamPath) {
9556
9763
  }
9557
9764
  return false;
9558
9765
  }
9559
- var digestPathFor = (stateDir, routeLabel, role, runId) => join22(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9766
+ var digestPathFor = (stateDir, routeLabel, role, runId) => join23(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9560
9767
  function planAgentRun(o) {
9561
- const briefPath = join22(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9768
+ const briefPath = join23(o.route.promptsDir, "personas", `lane-${o.role}.md`);
9562
9769
  if (!existsSync21(briefPath)) {
9563
9770
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
9564
9771
  }
9565
- const commonPath = join22(o.route.promptsDir, "personas", "common.md");
9772
+ const commonPath = join23(o.route.promptsDir, "personas", "common.md");
9566
9773
  const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync21(commonPath);
9567
9774
  const now = o.now ?? (() => Date.now());
9568
9775
  const routeLabel = routeSlug(o.route.route);
@@ -9597,7 +9804,7 @@ function planAgentRun(o) {
9597
9804
  digest
9598
9805
  });
9599
9806
  const promptSha = createHash4("sha256").update(expanded.text).digest("hex");
9600
- const streamsDir = join22(o.stateDir, "streams");
9807
+ const streamsDir = join23(o.stateDir, "streams");
9601
9808
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
9602
9809
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
9603
9810
  const { model } = seat;
@@ -9658,6 +9865,7 @@ function planAgentRun(o) {
9658
9865
  workspaceAuth: toolchain.auth === "workspace",
9659
9866
  provider: toolchainProvider(toolchain),
9660
9867
  maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
9868
+ ...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
9661
9869
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
9662
9870
  warnings: [...seat.warnings ?? [], ...expanded.warnings],
9663
9871
  prompt,
@@ -9682,8 +9890,8 @@ function planAgentRun(o) {
9682
9890
  digestAttached: digest !== void 0,
9683
9891
  digestAgeSeconds: ageSeconds,
9684
9892
  model,
9685
- streamPath: join22(streamsDir, `${base}.jsonl`),
9686
- eventsPath: join22(streamsDir, `${base}.events.jsonl`),
9893
+ streamPath: join23(streamsDir, `${base}.jsonl`),
9894
+ eventsPath: join23(streamsDir, `${base}.events.jsonl`),
9687
9895
  ticket: o.ticket,
9688
9896
  ticketRecordId: o.ticketRecordId ?? null,
9689
9897
  repo: o.repo ?? null,
@@ -9747,7 +9955,7 @@ function identityOf(plan, adapterHarness) {
9747
9955
  promptSha: plan.promptSha
9748
9956
  };
9749
9957
  }
9750
- async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes) {
9958
+ async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
9751
9959
  const elapsedMs = Date.now() - Date.parse(spawnedAt);
9752
9960
  const limit = limitMinutes ?? Math.round(elapsedMs / 6e4);
9753
9961
  emit.emit(`run_killed: ${role} run exceeded its wall-clock limit of ${limit} min (ran ${Math.round(elapsedMs / 6e4)} min)`, {
@@ -9755,7 +9963,7 @@ async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes)
9755
9963
  ...ticket ? { ticket } : {},
9756
9964
  data: { event: "run_killed", reason: WALL_CLOCK_REASON, limitMinutes: limit, elapsedMs, pid }
9757
9965
  });
9758
- await killRunGroup(pid);
9966
+ await kill();
9759
9967
  return { limitMinutes: limit, elapsedMs };
9760
9968
  }
9761
9969
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
@@ -9943,7 +10151,7 @@ var RunObserver = class {
9943
10151
  };
9944
10152
  function writeRunMeta(id, startedAt, liveTicket, extra) {
9945
10153
  try {
9946
- writeFileSync13(`${id.streamPath}.meta.json`, JSON.stringify({
10154
+ writeFileSync14(`${id.streamPath}.meta.json`, JSON.stringify({
9947
10155
  route: id.route,
9948
10156
  role: id.role,
9949
10157
  hint: id.ticket,
@@ -10050,7 +10258,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10050
10258
  if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
10051
10259
  if (sent.counterRetry) {
10052
10260
  try {
10053
- queueTicketCounters(stateDirOfStream(id.streamPath), basename7(id.streamPath, ".jsonl"), {
10261
+ queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
10054
10262
  version: 1,
10055
10263
  route: id.route,
10056
10264
  role: id.role,
@@ -10065,7 +10273,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10065
10273
  } catch (e) {
10066
10274
  if (e instanceof AgentLogUnreachableError) {
10067
10275
  try {
10068
- queueAgentRun(stateDirOfStream(id.streamPath), basename7(id.streamPath, ".jsonl"), {
10276
+ queueAgentRun(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
10069
10277
  version: 1,
10070
10278
  route: id.route,
10071
10279
  role: id.role,
@@ -10095,102 +10303,125 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10095
10303
  }
10096
10304
  function spawnAgent(plan, emit, hintMoved) {
10097
10305
  const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
10306
+ const runner = runnerFor(plan.runner, plan.sandboxConfig);
10098
10307
  const started = Date.now();
10099
10308
  const startedAt = new Date(started).toISOString();
10100
10309
  for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
10101
10310
  const eventsSink = openSink(plan.eventsPath);
10102
10311
  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
- });
10312
+ let streamFd;
10313
+ try {
10314
+ mkdirSync12(dirname8(plan.streamPath), { recursive: true });
10315
+ streamFd = openSync3(plan.streamPath, "a");
10316
+ } catch (e) {
10317
+ void endSink(eventsSink);
10318
+ return Promise.reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
10319
+ }
10320
+ const failed = (e) => {
10321
+ void endSink(eventsSink);
10322
+ return Promise.reject(new AgentSpawnError(`cannot run ${plan.bin}: ${e.message}`));
10323
+ };
10324
+ let begun;
10325
+ try {
10326
+ begun = runner.start(plan, { env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv }, streamFd, emit });
10327
+ } catch (e) {
10119
10328
  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 {
10329
+ return failed(e);
10330
+ }
10331
+ if (begun instanceof Promise) {
10332
+ return begun.then(
10333
+ (run2) => {
10334
+ closeSync3(streamFd);
10335
+ return supervise(run2);
10336
+ },
10337
+ (e) => {
10338
+ closeSync3(streamFd);
10339
+ return failed(e);
10146
10340
  }
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) };
10341
+ );
10342
+ }
10343
+ closeSync3(streamFd);
10344
+ return supervise(begun);
10345
+ function supervise({ child, handle, promptDelivered }) {
10346
+ return new Promise((resolve7, reject) => {
10347
+ const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
10348
+ const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
10349
+ if (child.pid !== void 0) {
10350
+ const sidecar = {
10351
+ pid: child.pid,
10352
+ ownerPid: process.pid,
10353
+ spawnedAt,
10354
+ role: plan.role,
10355
+ route: plan.route,
10356
+ ...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
10357
+ ...handle.runner === "sandbox" ? { runner: "sandbox", sandbox: handle.sandbox, sandboxBin: handle.sandboxBin } : {},
10358
+ ticket: plan.ticket ?? null,
10359
+ ticketRecordId: plan.ticketRecordId ?? null,
10360
+ repo: plan.repo ?? null,
10361
+ routeName: plan.routeName,
10362
+ model: plan.model,
10363
+ toolchain: id.toolchain,
10364
+ harness: id.harness,
10365
+ provider: id.provider,
10366
+ eventsPath: plan.eventsPath,
10367
+ agentLog: id.agentLog,
10368
+ promptVersion: plan.promptVersion,
10369
+ promptSha: plan.promptSha
10370
+ };
10371
+ try {
10372
+ writeFileSync14(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
10373
+ } catch {
10374
+ }
10177
10375
  }
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" });
10376
+ const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
10377
+ const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
10378
+ obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
10379
+ if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
10380
+ const tail3 = new FileTail(plan.streamPath);
10381
+ const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
10382
+ let killed;
10383
+ const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
10384
+ void killForDeadline(() => runner.kill(handle, WALL_CLOCK_REASON), child.pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
10385
+ killed = k;
10386
+ });
10387
+ }, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
10388
+ let spawnFailed = false;
10389
+ child.on("error", (err3) => {
10390
+ spawnFailed = true;
10391
+ clearInterval(timer);
10392
+ clearTimeout(limitTimer);
10393
+ void runner.release(handle, emit);
10394
+ reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
10395
+ });
10396
+ child.on("close", async (code) => {
10397
+ clearInterval(timer);
10398
+ clearTimeout(limitTimer);
10399
+ obs.feed(tail3.poll());
10400
+ obs.end();
10401
+ await endSink(eventsSink);
10402
+ if (spawnFailed) return;
10403
+ await runner.release(handle, emit);
10404
+ if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
10405
+ killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
10406
+ }
10407
+ const { ms: ms3, handoffPending } = await finishRun(
10408
+ id,
10409
+ obs,
10410
+ emit,
10411
+ startedAt,
10412
+ { code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
10413
+ plan.agentLog?.apiKey,
10414
+ hintMoved
10415
+ );
10416
+ resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
10417
+ });
10418
+ child.stdin?.on("error", (err3) => {
10419
+ emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
10420
+ });
10421
+ if (promptDelivered) child.stdin?.end();
10422
+ else child.stdin?.end(plan.prompt);
10191
10423
  });
10192
- child.stdin?.end(plan.prompt);
10193
- });
10424
+ }
10194
10425
  }
10195
10426
  async function reattachRun(streamPath, sc, emit, o = {}) {
10196
10427
  const adapter = adapterFor(sc.harness);
@@ -10202,6 +10433,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
10202
10433
  obs.muted = true;
10203
10434
  obs.feed(tail3.poll());
10204
10435
  obs.muted = false;
10436
+ const runner = runnerOfSidecar(sc);
10205
10437
  const adopted = isAlive(sc);
10206
10438
  let killed;
10207
10439
  if (adopted) {
@@ -10210,7 +10442,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
10210
10442
  while (isAlive(sc)) {
10211
10443
  obs.feed(tail3.poll());
10212
10444
  if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
10213
- killed = await killForDeadline(sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
10445
+ 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
10446
  continue;
10215
10447
  }
10216
10448
  await new Promise((r) => setTimeout(r, pollMs));
@@ -10219,6 +10451,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
10219
10451
  obs.feed(tail3.poll());
10220
10452
  obs.end();
10221
10453
  await endSink(eventsSink);
10454
+ await runner.release(runner.reattach(sc), emit);
10222
10455
  const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
10223
10456
  const { ms: ms3, ticket } = await finishRun(
10224
10457
  id,
@@ -10528,7 +10761,7 @@ var realDeps = {
10528
10761
  resolve7(r);
10529
10762
  }
10530
10763
  };
10531
- const child = spawn3(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
10764
+ const child = spawn4(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
10532
10765
  const killGroup = () => {
10533
10766
  try {
10534
10767
  if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
@@ -10673,7 +10906,7 @@ async function runSelftest(i, deps = realDeps) {
10673
10906
  ...adapter.runEnv(spec),
10674
10907
  [KEY_VAR]: i.apiKey
10675
10908
  };
10676
- const cwd = mkdtempSync(join23(tmpdir2(), "crew-selftest-"));
10909
+ const cwd = mkdtempSync2(join24(tmpdir3(), "crew-selftest-"));
10677
10910
  try {
10678
10911
  const run2 = await deps.run({
10679
10912
  bin: invocation.bin,
@@ -10695,7 +10928,7 @@ async function runSelftest(i, deps = realDeps) {
10695
10928
  return finish({ ok: false, failure, detail, ...usage2 });
10696
10929
  } finally {
10697
10930
  try {
10698
- rmSync12(cwd, { recursive: true, force: true });
10931
+ rmSync13(cwd, { recursive: true, force: true });
10699
10932
  } catch {
10700
10933
  }
10701
10934
  }
@@ -10706,7 +10939,7 @@ function formatResult(r) {
10706
10939
  const secs = `${(r.ms / 1e3).toFixed(1)}s`;
10707
10940
  return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
10708
10941
  }
10709
- var fileFor2 = (stateDir) => join23(stateDir, "selftest.json");
10942
+ var fileFor2 = (stateDir) => join24(stateDir, "selftest.json");
10710
10943
  function readSelftest(stateDir) {
10711
10944
  try {
10712
10945
  const p = fileFor2(stateDir);
@@ -10719,7 +10952,7 @@ function recordSelftest(stateDir, results) {
10719
10952
  const all = readSelftest(stateDir);
10720
10953
  for (const r of results) all[r.toolchain] = r;
10721
10954
  mkdirSync13(stateDir, { recursive: true });
10722
- writeFileSync14(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10955
+ writeFileSync15(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
10723
10956
  `);
10724
10957
  }
10725
10958
  function doctorSelftestLines(stored, toolchains) {
@@ -10731,7 +10964,7 @@ function doctorSelftestLines(stored, toolchains) {
10731
10964
  }
10732
10965
 
10733
10966
  // src/cli.ts
10734
- import { resolve as resolve6, dirname as dirname25, join as join47, basename as basename14 } from "path";
10967
+ import { resolve as resolve6, dirname as dirname25, join as join48, basename as basename15 } from "path";
10735
10968
  import { fileURLToPath } from "url";
10736
10969
  import { hostname as hostname4, homedir as homedir11 } from "os";
10737
10970
  import { spawnSync as spawnSync10 } from "child_process";
@@ -10928,10 +11161,10 @@ function rosterMarkdown(roster, meId) {
10928
11161
  }
10929
11162
 
10930
11163
  // 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";
11164
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
11165
+ import { dirname as dirname9, join as join25 } from "path";
10933
11166
  function lastSessionFile(stateDir) {
10934
- return join24(stateDir, "connect-session.json");
11167
+ return join25(stateDir, "connect-session.json");
10935
11168
  }
10936
11169
  function readPointers(stateDir) {
10937
11170
  try {
@@ -10950,7 +11183,7 @@ function readPointers(stateDir) {
10950
11183
  function writePointers(stateDir, pointers) {
10951
11184
  const file = lastSessionFile(stateDir);
10952
11185
  mkdirSync14(dirname9(file), { recursive: true });
10953
- writeFileSync15(file, `${JSON.stringify(pointers, null, 2)}
11186
+ writeFileSync16(file, `${JSON.stringify(pointers, null, 2)}
10954
11187
  `, { mode: 384 });
10955
11188
  }
10956
11189
  function rememberSession(stateDir, host, workspaceSlug) {
@@ -10967,7 +11200,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
10967
11200
  const { [host]: _old, ...rest } = all;
10968
11201
  const next = left.length ? { ...rest, [host]: left } : rest;
10969
11202
  if (Object.keys(next).length) writePointers(stateDir, next);
10970
- else rmSync13(lastSessionFile(stateDir), { force: true });
11203
+ else rmSync14(lastSessionFile(stateDir), { force: true });
10971
11204
  } catch {
10972
11205
  }
10973
11206
  }
@@ -11007,7 +11240,7 @@ function forgetSession(stateDir, host) {
11007
11240
  try {
11008
11241
  const { [host]: _gone, ...rest } = readPointers(stateDir);
11009
11242
  if (Object.keys(rest).length) writePointers(stateDir, rest);
11010
- else rmSync13(lastSessionFile(stateDir), { force: true });
11243
+ else rmSync14(lastSessionFile(stateDir), { force: true });
11011
11244
  } catch {
11012
11245
  }
11013
11246
  }
@@ -11035,11 +11268,11 @@ async function signOutSessions(o) {
11035
11268
  }
11036
11269
 
11037
11270
  // 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";
11271
+ import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync9, rmSync as rmSync15, statSync as statSync8, writeFileSync as writeFileSync17 } from "fs";
11272
+ import { dirname as dirname10, join as join26 } from "path";
11040
11273
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
11041
11274
  function clonedCheckoutsFile(stateDir) {
11042
- return join25(stateDir, "cloned-checkouts.json");
11275
+ return join26(stateDir, "cloned-checkouts.json");
11043
11276
  }
11044
11277
  function readClonedCheckouts(stateDir) {
11045
11278
  try {
@@ -11054,7 +11287,7 @@ function recordClonedCheckout(stateDir, dir) {
11054
11287
  if (known.includes(dir)) return;
11055
11288
  const file = clonedCheckoutsFile(stateDir);
11056
11289
  mkdirSync15(dirname10(file), { recursive: true });
11057
- writeFileSync16(file, `${JSON.stringify([...known, dir], null, 2)}
11290
+ writeFileSync17(file, `${JSON.stringify([...known, dir], null, 2)}
11058
11291
  `);
11059
11292
  }
11060
11293
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -11109,7 +11342,7 @@ async function runReset(o, fx) {
11109
11342
  if (o.beforeState) steps.push(...await o.beforeState());
11110
11343
  steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
11111
11344
  if (existsSync23(o.configFile)) {
11112
- if (!o.dryRun) rmSync14(o.configFile, { force: true });
11345
+ if (!o.dryRun) rmSync15(o.configFile, { force: true });
11113
11346
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
11114
11347
  } else {
11115
11348
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -11125,21 +11358,21 @@ function removeCheckout(dir, dryRun, fx) {
11125
11358
  if (linkedWorktrees.length) {
11126
11359
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
11127
11360
  }
11128
- if (!dryRun) rmSync14(dir, { recursive: true, force: true });
11361
+ if (!dryRun) rmSync15(dir, { recursive: true, force: true });
11129
11362
  return { step: "checkout", target: dir, action: mark(dryRun) };
11130
11363
  }
11131
11364
  function removeState(stateDir, dryRun) {
11132
11365
  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")];
11366
+ const targets = [join26(stateDir, "resolved"), join26(stateDir, "keys"), clonedCheckoutsFile(stateDir), join26(stateDir, "connect-session.json"), join26(stateDir, "reload-requested"), join26(stateDir, "daemon.started")];
11134
11367
  try {
11135
11368
  for (const name of readdirSync9(stateDir)) {
11136
- if (name.startsWith(".") && statSync8(join25(stateDir, name)).isFile()) targets.push(join25(stateDir, name));
11369
+ if (name.startsWith(".") && statSync8(join26(stateDir, name)).isFile()) targets.push(join26(stateDir, name));
11137
11370
  }
11138
11371
  } catch {
11139
11372
  }
11140
11373
  for (const t of targets) {
11141
11374
  if (!existsSync23(t)) continue;
11142
- if (!dryRun) rmSync14(t, { recursive: true, force: true });
11375
+ if (!dryRun) rmSync15(t, { recursive: true, force: true });
11143
11376
  out.push({ step: "state", target: t, action: mark(dryRun) });
11144
11377
  }
11145
11378
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -11153,7 +11386,7 @@ function wipeStateDir(stateDir, dryRun) {
11153
11386
  return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
11154
11387
  }
11155
11388
  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 });
11389
+ if (!dryRun) for (const n of names) rmSync15(join26(stateDir, n), { recursive: true, force: true });
11157
11390
  return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
11158
11391
  }
11159
11392
  function describeResetStep(s) {
@@ -11239,7 +11472,7 @@ import { existsSync as existsSync26, readdirSync as readdirSync12 } from "fs";
11239
11472
 
11240
11473
  // src/interrupted.ts
11241
11474
  import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
11242
- import { join as join26 } from "path";
11475
+ import { join as join27 } from "path";
11243
11476
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
11244
11477
  function interruptedComment(b) {
11245
11478
  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.";
@@ -11266,13 +11499,13 @@ function runFinished(streamPath) {
11266
11499
  }
11267
11500
  }
11268
11501
  function orphanedStreams(o) {
11269
- const dir = join26(o.stateDir, "streams");
11502
+ const dir = join27(o.stateDir, "streams");
11270
11503
  if (!existsSync24(dir)) return [];
11271
11504
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
11272
11505
  const out = [];
11273
11506
  for (const name of readdirSync10(dir)) {
11274
11507
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
11275
- const streamPath = join26(dir, name);
11508
+ const streamPath = join27(dir, name);
11276
11509
  if (runFinished(streamPath)) continue;
11277
11510
  const sidecar = readRunSidecar(streamPath);
11278
11511
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -11326,14 +11559,14 @@ async function reconcileRuns(o) {
11326
11559
 
11327
11560
  // src/live-runs.ts
11328
11561
  import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
11329
- import { join as join27 } from "path";
11562
+ import { join as join28 } from "path";
11330
11563
  function liveRuns(stateDir, isAlive = runIsAlive) {
11331
- const dir = join27(stateDir, "streams");
11564
+ const dir = join28(stateDir, "streams");
11332
11565
  if (!existsSync25(dir)) return [];
11333
11566
  const out = [];
11334
11567
  for (const name of readdirSync11(dir)) {
11335
11568
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
11336
- const streamPath = join27(dir, name);
11569
+ const streamPath = join28(dir, name);
11337
11570
  if (runFinished(streamPath)) continue;
11338
11571
  const sidecar = readRunSidecar(streamPath);
11339
11572
  if (!sidecar || !isAlive(sidecar)) continue;
@@ -11395,11 +11628,11 @@ function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
11395
11628
  }
11396
11629
 
11397
11630
  // src/passenger-containers.ts
11398
- import { execFileSync as execFileSync5 } from "child_process";
11631
+ import { execFileSync as execFileSync6 } from "child_process";
11399
11632
  import { createHash as createHash5 } from "crypto";
11400
11633
  var realExec = (cmd, args, opts) => {
11401
11634
  try {
11402
- return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
11635
+ return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
11403
11636
  } catch (e) {
11404
11637
  const err3 = e;
11405
11638
  const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -11708,9 +11941,9 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
11708
11941
  }
11709
11942
 
11710
11943
  // src/pool-member.ts
11711
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync24, renameSync as renameSync3, writeFileSync as writeFileSync17 } from "fs";
11944
+ import { mkdirSync as mkdirSync16, readFileSync as readFileSync24, renameSync as renameSync3, writeFileSync as writeFileSync18 } from "fs";
11712
11945
  import { homedir as homedir8 } from "os";
11713
- import { dirname as dirname11, join as join28 } from "path";
11946
+ import { dirname as dirname11, join as join29 } from "path";
11714
11947
  var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
11715
11948
  function backoffMs(attempt) {
11716
11949
  return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
@@ -11899,10 +12132,10 @@ async function runPoolMember(deps) {
11899
12132
  }
11900
12133
  }
11901
12134
  function defaultMemberDir(env = process.env) {
11902
- return env.CREW_POOL_DIR ?? join28(env.XDG_STATE_HOME ?? join28(homedir8(), ".local", "state"), "crew-pool-member");
12135
+ return env.CREW_POOL_DIR ?? join29(env.XDG_STATE_HOME ?? join29(homedir8(), ".local", "state"), "crew-pool-member");
11903
12136
  }
11904
12137
  function fileMemberStore(dir) {
11905
- const file = join28(dir, "member.json");
12138
+ const file = join29(dir, "member.json");
11906
12139
  return {
11907
12140
  read() {
11908
12141
  try {
@@ -11914,7 +12147,7 @@ function fileMemberStore(dir) {
11914
12147
  write(f) {
11915
12148
  mkdirSync16(dirname11(file), { recursive: true, mode: 448 });
11916
12149
  const tmp = `${file}.tmp`;
11917
- writeFileSync17(tmp, `${JSON.stringify(f)}
12150
+ writeFileSync18(tmp, `${JSON.stringify(f)}
11918
12151
  `, { mode: 384 });
11919
12152
  renameSync3(tmp, file);
11920
12153
  }
@@ -11970,18 +12203,18 @@ import { existsSync as existsSync30 } from "fs";
11970
12203
  import { hostname as hostname3 } from "os";
11971
12204
 
11972
12205
  // src/machine-id.ts
11973
- import { execFileSync as execFileSync6 } from "child_process";
12206
+ import { execFileSync as execFileSync7 } from "child_process";
11974
12207
  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";
12208
+ import { existsSync as existsSync27, mkdirSync as mkdirSync17, readFileSync as readFileSync25, writeFileSync as writeFileSync19 } from "fs";
12209
+ import { dirname as dirname12, join as join30 } from "path";
11977
12210
  function machineIdPathFor(stateDir) {
11978
- return join29(stateDir, "machine-id");
12211
+ return join30(stateDir, "machine-id");
11979
12212
  }
11980
12213
  var sha256 = (s) => createHash6("sha256").update(s).digest("hex");
11981
12214
  function readRawMachineId(platform2 = process.platform) {
11982
12215
  try {
11983
12216
  if (platform2 === "darwin") {
11984
- const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
12217
+ const out = execFileSync7("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
11985
12218
  encoding: "utf8",
11986
12219
  stdio: ["ignore", "pipe", "ignore"]
11987
12220
  });
@@ -12008,7 +12241,7 @@ function ensureMachineId(stateDir, opts = {}) {
12008
12241
  if (!raw) return void 0;
12009
12242
  const hashed = sha256(raw);
12010
12243
  mkdirSync17(dirname12(path), { recursive: true });
12011
- writeFileSync18(path, `${hashed}
12244
+ writeFileSync19(path, `${hashed}
12012
12245
  `, { mode: 384 });
12013
12246
  return hashed;
12014
12247
  }
@@ -12021,7 +12254,7 @@ function matchShipRows(ships, shipName2, machineId) {
12021
12254
 
12022
12255
  // src/provision.ts
12023
12256
  import { spawnSync as spawnSync7 } from "child_process";
12024
- import { existsSync as existsSync28, readFileSync as readFileSync26, writeFileSync as writeFileSync19 } from "fs";
12257
+ import { existsSync as existsSync28, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
12025
12258
  import { hostname as hostname2 } from "os";
12026
12259
  import { parseDocument as parseDocument3 } from "yaml";
12027
12260
 
@@ -12030,7 +12263,7 @@ import { parseDocument as parseDocument2 } from "yaml";
12030
12263
 
12031
12264
  // src/repos-cmd.ts
12032
12265
  import { homedir as homedir9 } from "os";
12033
- import { basename as basename8, join as join30, resolve as resolve3 } from "path";
12266
+ import { basename as basename9, join as join31, resolve as resolve3 } from "path";
12034
12267
  import { isMap, isSeq, parseDocument, stringify } from "yaml";
12035
12268
  var ReposError = class extends Error {
12036
12269
  };
@@ -12102,7 +12335,7 @@ function configuredRepos(text2, route2, base) {
12102
12335
  });
12103
12336
  }
12104
12337
  const dir = m.get("dir");
12105
- return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
12338
+ return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
12106
12339
  }
12107
12340
  function cloneBaseOf(text2, route2, base, isCheckout) {
12108
12341
  const m = routeMapOf(parseDocument(text2), route2);
@@ -12191,7 +12424,7 @@ function planRepoAdd(i) {
12191
12424
  throw new ReposError(`${target} is already configured on route ${i.route}`);
12192
12425
  }
12193
12426
  const matched = matchRepoName(i.origin, i.resolved);
12194
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
12427
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
12195
12428
  let warning;
12196
12429
  if (!matched) {
12197
12430
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -12308,7 +12541,7 @@ function planRepoClone(i) {
12308
12541
  }
12309
12542
  const ship = doc.get("ship", true);
12310
12543
  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);
12544
+ const target = i.dir ? expandDir(i.dir, i.base) : join31(expandDir(basePath, i.base), i.route.split("/")[0], name);
12312
12545
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
12313
12546
  const forgeHost = i.resolved?.repoForgeHosts?.[id];
12314
12547
  return { ...add, remote, target, ...forgeHost ? { forgeHost } : {} };
@@ -12587,7 +12820,7 @@ function realProvisionDeps(crewHome) {
12587
12820
  const entry = routes?.items?.find((r) => r.get("route") === route2);
12588
12821
  if (!entry) throw new Error(`route ${route2} is not in ${file}`);
12589
12822
  if (entry.get("enabled") === true) return false;
12590
- writeFileSync19(file, planEnable(text2, route2));
12823
+ writeFileSync20(file, planEnable(text2, route2));
12591
12824
  return true;
12592
12825
  },
12593
12826
  installState() {
@@ -12634,7 +12867,7 @@ function realProvisionDeps(crewHome) {
12634
12867
  const doc = parseDocument3(readFileSync26(file, "utf8"));
12635
12868
  if (doc.getIn(["ship", key]) === value2) return false;
12636
12869
  doc.setIn(["ship", key], value2);
12637
- writeFileSync19(file, doc.toString());
12870
+ writeFileSync20(file, doc.toString());
12638
12871
  return true;
12639
12872
  },
12640
12873
  storedToken(host) {
@@ -12650,12 +12883,12 @@ host=${host}
12650
12883
  }
12651
12884
 
12652
12885
  // 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";
12886
+ import { spawn as spawn5 } from "child_process";
12887
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync27, writeFileSync as writeFileSync21, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
12655
12888
  import { randomInt } from "crypto";
12656
- import { join as join31 } from "path";
12889
+ import { join as join32 } from "path";
12657
12890
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
12658
- var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
12891
+ var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
12659
12892
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
12660
12893
  var SLUG_LENGTH = 12;
12661
12894
  function generateTunnelSlug() {
@@ -12725,7 +12958,7 @@ function startTunnel(opts) {
12725
12958
  };
12726
12959
  }
12727
12960
  function tunnelStatePath(stateDir, workspaceId) {
12728
- return join31(stateDir, "passengers", `${workspaceId}.json`);
12961
+ return join32(stateDir, "passengers", `${workspaceId}.json`);
12729
12962
  }
12730
12963
  function readPersistedTunnel(stateDir, workspaceId) {
12731
12964
  const path = tunnelStatePath(stateDir, workspaceId);
@@ -12738,8 +12971,8 @@ function readPersistedTunnel(stateDir, workspaceId) {
12738
12971
  }
12739
12972
  function writePersistedTunnel(stateDir, state) {
12740
12973
  const path = tunnelStatePath(stateDir, state.workspaceId);
12741
- mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
12742
- writeFileSync20(path, `${JSON.stringify(state, null, 2)}
12974
+ mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
12975
+ writeFileSync21(path, `${JSON.stringify(state, null, 2)}
12743
12976
  `);
12744
12977
  }
12745
12978
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -12841,7 +13074,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12841
13074
  }
12842
13075
  }
12843
13076
  function readAllPersistedWorkspaceIds(stateDir) {
12844
- const dir = join31(stateDir, "passengers");
13077
+ const dir = join32(stateDir, "passengers");
12845
13078
  if (!existsSync29(dir)) return [];
12846
13079
  return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12847
13080
  }
@@ -13708,8 +13941,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
13708
13941
  }
13709
13942
 
13710
13943
  // src/poll.ts
13711
- import { writeFileSync as writeFileSync21 } from "fs";
13712
- import { join as join32 } from "path";
13944
+ import { writeFileSync as writeFileSync22 } from "fs";
13945
+ import { join as join33 } from "path";
13713
13946
 
13714
13947
  // src/stalled.ts
13715
13948
  var STALLED_AFTER_CYCLES = 30;
@@ -14016,7 +14249,7 @@ async function decideCycle(o) {
14016
14249
  }
14017
14250
  return decision;
14018
14251
  }
14019
- var digestPath = (stateDir, route2, role, runId) => join32(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
14252
+ var digestPath = (stateDir, route2, role, runId) => join33(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
14020
14253
  function otherRunsRemoved(tickets, own) {
14021
14254
  if (!own) return tickets;
14022
14255
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -14094,7 +14327,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
14094
14327
 
14095
14328
  ${body}` : body;
14096
14329
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
14097
- writeFileSync21(path, text2);
14330
+ writeFileSync22(path, text2);
14098
14331
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
14099
14332
  data: { role, bytes: Buffer.byteLength(text2) }
14100
14333
  });
@@ -14202,7 +14435,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
14202
14435
  }
14203
14436
 
14204
14437
  // src/repo-refresh.ts
14205
- import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync22 } from "fs";
14438
+ import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync23 } from "fs";
14206
14439
  import { dirname as dirname14 } from "path";
14207
14440
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
14208
14441
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
@@ -14236,7 +14469,7 @@ function persistRepoRows(stateDir, routeName, rows) {
14236
14469
  const raw = JSON.parse(readFileSync28(path, "utf8"));
14237
14470
  applyRepoRows(raw, rows);
14238
14471
  mkdirSync19(dirname14(path), { recursive: true });
14239
- writeFileSync22(path, `${JSON.stringify(raw, null, 2)}
14472
+ writeFileSync23(path, `${JSON.stringify(raw, null, 2)}
14240
14473
  `);
14241
14474
  }
14242
14475
  async function refreshUnknownRepos(o) {
@@ -14686,8 +14919,8 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
14686
14919
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
14687
14920
 
14688
14921
  // 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";
14922
+ import { delimiter, dirname as dirname15, join as join34 } from "path";
14923
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync20, writeFileSync as writeFileSync24 } from "fs";
14691
14924
  import { stringify as stringify2 } from "yaml";
14692
14925
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
14693
14926
  function shouldRunWizard(opts) {
@@ -14709,7 +14942,7 @@ function defaultIsExecutable(p) {
14709
14942
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
14710
14943
  for (const dir of pathEnv.split(delimiter)) {
14711
14944
  if (!dir) continue;
14712
- const candidate = join33(dir, name);
14945
+ const candidate = join34(dir, name);
14713
14946
  if (isExecutable(candidate)) return candidate;
14714
14947
  }
14715
14948
  return void 0;
@@ -14772,7 +15005,7 @@ ${routeBlock}`;
14772
15005
  function writeNewConfig(path, text2) {
14773
15006
  mkdirSync20(dirname15(path), { recursive: true });
14774
15007
  try {
14775
- writeFileSync23(path, text2, { mode: 384, flag: "wx" });
15008
+ writeFileSync24(path, text2, { mode: 384, flag: "wx" });
14776
15009
  return true;
14777
15010
  } catch (e) {
14778
15011
  if (e.code === "EEXIST") return false;
@@ -14806,7 +15039,7 @@ Wrote ${o.configPath}.
14806
15039
  // src/connect-json.ts
14807
15040
  import { accessSync as accessSync4, constants as constants4 } from "fs";
14808
15041
  import { homedir as homedir10 } from "os";
14809
- import { dirname as dirname16, join as join34 } from "path";
15042
+ import { dirname as dirname16, join as join35 } from "path";
14810
15043
  import { delimiter as delimiter2 } from "path";
14811
15044
  var CHOICE_EXIT_CODE = 3;
14812
15045
  function createEmitter(json2, write) {
@@ -14845,12 +15078,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
14845
15078
  const dirs = [
14846
15079
  ...pathEnv.split(delimiter2),
14847
15080
  ...(extraPath ?? "").split(delimiter2),
14848
- join34(home, ".local/bin"),
14849
- join34(home, ".claude/local"),
15081
+ join35(home, ".local/bin"),
15082
+ join35(home, ".claude/local"),
14850
15083
  "/opt/homebrew/bin",
14851
15084
  "/usr/local/bin",
14852
- join34(home, ".npm-global/bin"),
14853
- join34(home, ".volta/bin")
15085
+ join35(home, ".npm-global/bin"),
15086
+ join35(home, ".volta/bin")
14854
15087
  ];
14855
15088
  return [...new Set(dirs.filter(Boolean))];
14856
15089
  }
@@ -14886,9 +15119,9 @@ function flagWizardAnswers(f, env) {
14886
15119
  }
14887
15120
 
14888
15121
  // 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";
15122
+ import { readFileSync as readFileSync29, writeFileSync as writeFileSync25, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync3, rmSync as rmSync16 } from "fs";
15123
+ import { dirname as dirname17, join as join36 } from "path";
15124
+ import { tmpdir as tmpdir4 } from "os";
14892
15125
 
14893
15126
  // src/release.ts
14894
15127
  function describeUnplaceable(unplaceable, verifiedStatus) {
@@ -15628,7 +15861,7 @@ async function bump(o, size) {
15628
15861
  }
15629
15862
  return printed;
15630
15863
  }
15631
- const files = o.repo.release.versionFiles.map((f) => join35(o.cwd, f));
15864
+ const files = o.repo.release.versionFiles.map((f) => join36(o.cwd, f));
15632
15865
  const first = files[0];
15633
15866
  if (!existsSync33(first)) {
15634
15867
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
@@ -15653,21 +15886,21 @@ async function bump(o, size) {
15653
15886
  o.emit.error(`no version field in ${f}`);
15654
15887
  return null;
15655
15888
  }
15656
- writeFileSync24(f, out);
15889
+ writeFileSync25(f, out);
15657
15890
  }
15658
15891
  return version;
15659
15892
  }
15660
15893
  function writeChangelog(o, version, entries) {
15661
15894
  const file = o.repo.release.changelog;
15662
15895
  if (!file || entries.length === 0) return;
15663
- const path = join35(o.cwd, file);
15896
+ const path = join36(o.cwd, file);
15664
15897
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
15665
15898
  if (o.dryRun) {
15666
15899
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
15667
15900
  return;
15668
15901
  }
15669
15902
  const existing = existsSync33(path) ? readFileSync29(path, "utf8") : "# Changelog\n";
15670
- writeFileSync24(path, insertChangelogSection(existing, section));
15903
+ writeFileSync25(path, insertChangelogSection(existing, section));
15671
15904
  }
15672
15905
  async function confirm(o, expected) {
15673
15906
  if (!o.repo.hooks.released) return void 0;
@@ -15750,7 +15983,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
15750
15983
  run2(cwd, ["worktree", "remove", "--force", dir]);
15751
15984
  } finally {
15752
15985
  try {
15753
- rmSync15(root, { recursive: true, force: true });
15986
+ rmSync16(root, { recursive: true, force: true });
15754
15987
  } finally {
15755
15988
  run2(cwd, ["worktree", "prune"]);
15756
15989
  run2(cwd, ["branch", "-D", branch]);
@@ -15760,8 +15993,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
15760
15993
  function openReleaseCheckout(o) {
15761
15994
  const { remote, base } = o.repo.branch;
15762
15995
  const branch = `crew/release-${Date.now()}`;
15763
- const root = mkdtempSync2(join35(tmpdir3(), "crew-release-"));
15764
- const dir = join35(root, "checkout");
15996
+ const root = mkdtempSync3(join36(tmpdir4(), "crew-release-"));
15997
+ const dir = join36(root, "checkout");
15765
15998
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
15766
15999
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
15767
16000
  try {
@@ -15771,10 +16004,10 @@ function openReleaseCheckout(o) {
15771
16004
  throw e;
15772
16005
  }
15773
16006
  for (const f of o.repo.worktrees.copy) {
15774
- const src = join35(o.cwd, f);
16007
+ const src = join36(o.cwd, f);
15775
16008
  if (!existsSync33(src)) continue;
15776
- mkdirSync21(dirname17(join35(dir, f)), { recursive: true });
15777
- copyFileSync(src, join35(dir, f));
16009
+ mkdirSync21(dirname17(join36(dir, f)), { recursive: true });
16010
+ copyFileSync(src, join36(dir, f));
15778
16011
  }
15779
16012
  return { dir, branch, close };
15780
16013
  }
@@ -15919,7 +16152,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
15919
16152
  let checkout;
15920
16153
  const stranded = recoverStrandedReleaseCheckouts(o.cwd);
15921
16154
  if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
15922
- const swept = sweepStaleReleaseRoots(tmpdir3());
16155
+ const swept = sweepStaleReleaseRoots(tmpdir4());
15923
16156
  if (swept.length > 0) o.emit.emit(scoped(`removed ${swept.length} stale crew-release-* temp dir(s)`), { step: "release" });
15924
16157
  try {
15925
16158
  checkout = openReleaseCheckout(o);
@@ -17092,7 +17325,7 @@ function nextRoles(decision, ran) {
17092
17325
  }
17093
17326
 
17094
17327
  // src/ship-name.ts
17095
- import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync30, renameSync as renameSync4, writeFileSync as writeFileSync25 } from "fs";
17328
+ import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync30, renameSync as renameSync4, writeFileSync as writeFileSync26 } from "fs";
17096
17329
  import { dirname as dirname18 } from "path";
17097
17330
  import { parseDocument as parseDocument5 } from "yaml";
17098
17331
  var ShipNameError = class extends Error {
@@ -17174,7 +17407,7 @@ function writeSyncedName(resolvedFile, name) {
17174
17407
  raw.shipName = name;
17175
17408
  mkdirSync22(dirname18(resolvedFile), { recursive: true });
17176
17409
  const tmp = `${resolvedFile}.tmp${process.pid}`;
17177
- writeFileSync25(tmp, `${JSON.stringify(raw, null, 2)}
17410
+ writeFileSync26(tmp, `${JSON.stringify(raw, null, 2)}
17178
17411
  `);
17179
17412
  renameSync4(tmp, resolvedFile);
17180
17413
  }
@@ -17206,7 +17439,7 @@ async function reconcileShipName(i) {
17206
17439
  if (result.status !== "ok") return result;
17207
17440
  if (result.action === "adopt" || result.action === "row-wins") {
17208
17441
  const text2 = readFileSync30(i.configFile, "utf8");
17209
- writeFileSync25(i.configFile, setShipNameInYaml(text2, result.name));
17442
+ writeFileSync26(i.configFile, setShipNameInYaml(text2, result.name));
17210
17443
  i.ship.name = result.name;
17211
17444
  if (result.action === "row-wins") {
17212
17445
  i.emit?.warn(
@@ -17865,7 +18098,7 @@ function describeCapability(i) {
17865
18098
  }
17866
18099
 
17867
18100
  // src/agents-sync-run.ts
17868
- import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync31, writeFileSync as writeFileSync26 } from "fs";
18101
+ import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync31, writeFileSync as writeFileSync27 } from "fs";
17869
18102
  import { dirname as dirname19 } from "path";
17870
18103
  async function runAgentsSync(target, o) {
17871
18104
  const dryRun = o.dryRun ?? false;
@@ -17904,7 +18137,7 @@ async function runAgentsSync(target, o) {
17904
18137
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
17905
18138
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
17906
18139
  mkdirSync23(dirname19(resolvedPath), { recursive: true });
17907
- writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
18140
+ writeFileSync27(resolvedPath, `${JSON.stringify(raw, null, 2)}
17908
18141
  `);
17909
18142
  if (target.resolved) {
17910
18143
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -18009,11 +18242,11 @@ async function showLogEntry(route2, entryId, opts = {}) {
18009
18242
  // src/repos-check.ts
18010
18243
  import { spawnSync as spawnSync9 } from "child_process";
18011
18244
  import { readFileSync as readFileSync32 } from "fs";
18012
- import { basename as basename9 } from "path";
18245
+ import { basename as basename10 } from "path";
18013
18246
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
18014
18247
  function checkHookSyntax(script, shell) {
18015
18248
  const spec = resolveShell(shell);
18016
- const name = basename9(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18249
+ const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18017
18250
  let run2;
18018
18251
  if (POSIX_SHELL.test(name)) {
18019
18252
  run2 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
@@ -18032,7 +18265,7 @@ function checkHookSyntax(script, shell) {
18032
18265
  return { status: "error", message };
18033
18266
  }
18034
18267
  function renderSamples(cfg, dir) {
18035
- const checkout = basename9(dir.replace(/[/\\]+$/, ""));
18268
+ const checkout = basename10(dir.replace(/[/\\]+$/, ""));
18036
18269
  const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
18037
18270
  const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
18038
18271
  return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
@@ -18324,7 +18557,7 @@ function existingRepoConfig(f) {
18324
18557
  }
18325
18558
 
18326
18559
  // src/repos-hooks-agent.ts
18327
- import { spawn as spawn5, execFileSync as execFileSync7 } from "child_process";
18560
+ import { spawn as spawn6, execFileSync as execFileSync8 } from "child_process";
18328
18561
  import { realpathSync as realpathSync2 } from "fs";
18329
18562
  import { isAbsolute as isAbsolute2, relative } from "path";
18330
18563
  var AgentSuggestError = class extends Error {
@@ -18427,7 +18660,7 @@ function classifyFailure(o) {
18427
18660
  }
18428
18661
  function porcelain(dir) {
18429
18662
  try {
18430
- return execFileSync7("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
18663
+ return execFileSync8("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
18431
18664
  } catch {
18432
18665
  return void 0;
18433
18666
  }
@@ -18445,7 +18678,7 @@ async function runAgentSuggest(o) {
18445
18678
  const run2 = await new Promise((resolve7) => {
18446
18679
  let out = "";
18447
18680
  let err3 = "";
18448
- const child = spawn5(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
18681
+ const child = spawn6(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
18449
18682
  let settled = false;
18450
18683
  const settle = (r) => {
18451
18684
  if (settled) return;
@@ -18496,7 +18729,7 @@ ${run2.err}` });
18496
18729
  }
18497
18730
 
18498
18731
  // src/sample-ticket.ts
18499
- import { basename as basename10, dirname as dirname20, join as join36 } from "path";
18732
+ import { basename as basename11, dirname as dirname20, join as join37 } from "path";
18500
18733
  var SAMPLE_REPO = "crew-sample";
18501
18734
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
18502
18735
  var SAMPLE_MARKER = "sample: true";
@@ -18519,8 +18752,8 @@ has something safe to work on: its test and build hooks always pass, it has no
18519
18752
  remote, and \`crew sample-ticket --remove\` deletes it.
18520
18753
  `;
18521
18754
  function sampleDir(existing, reposBase, workspace) {
18522
- if (existing.length > 0) return join36(dirname20(existing[0]), SAMPLE_REPO);
18523
- return join36(reposBase, workspace, SAMPLE_REPO);
18755
+ if (existing.length > 0) return join37(dirname20(existing[0]), SAMPLE_REPO);
18756
+ return join37(reposBase, workspace, SAMPLE_REPO);
18524
18757
  }
18525
18758
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
18526
18759
  function sampleDescription() {
@@ -18565,7 +18798,7 @@ function planSampleTicket(i) {
18565
18798
 
18566
18799
  // src/worktree-sweep.ts
18567
18800
  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";
18801
+ import { basename as basename12, dirname as dirname21, join as join38, resolve as resolve4 } from "path";
18569
18802
  function planWorktreeSweep(target, tickets, contract) {
18570
18803
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
18571
18804
  const verified = contract.statuses.verified;
@@ -18606,7 +18839,7 @@ function ticketForWorktree(w, byNumber, contract) {
18606
18839
  const t = num ? byNumber.get(num) : void 0;
18607
18840
  if (t && ticketStems(t).has(stem)) return t;
18608
18841
  }
18609
- const dir = basename11(w.path);
18842
+ const dir = basename12(w.path);
18610
18843
  const m = DIR_NUMBER.exec(dir);
18611
18844
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
18612
18845
  if (!w.branch) {
@@ -18622,7 +18855,7 @@ function worktreeTicketNumbers(target) {
18622
18855
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
18623
18856
  if (n) nums.add(n);
18624
18857
  }
18625
- const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
18858
+ const m = DIR_NUMBER_SUFFIXED.exec(basename12(w.path));
18626
18859
  if (m) nums.add(m[1]);
18627
18860
  }
18628
18861
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
@@ -18630,7 +18863,7 @@ function worktreeTicketNumbers(target) {
18630
18863
  }
18631
18864
  function siblingWorktreeDirs(target) {
18632
18865
  const parent = dirname21(target.dir.replace(/[/\\]+$/, ""));
18633
- const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
18866
+ const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
18634
18867
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
18635
18868
  let names;
18636
18869
  try {
@@ -18642,7 +18875,7 @@ function siblingWorktreeDirs(target) {
18642
18875
  for (const name of names) {
18643
18876
  if (!name.startsWith(stem)) continue;
18644
18877
  const m = DIR_NUMBER_SUFFIXED.exec(name);
18645
- const path = join37(parent, name);
18878
+ const path = join38(parent, name);
18646
18879
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
18647
18880
  }
18648
18881
  return out;
@@ -18655,7 +18888,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
18655
18888
  }
18656
18889
  function warnOrphanedWorktreeDirs(paths, log, held) {
18657
18890
  for (const p of paths) {
18658
- const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
18891
+ const number = DIR_NUMBER_SUFFIXED.exec(basename12(p))?.[1];
18659
18892
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
18660
18893
  if (when) {
18661
18894
  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" });
@@ -18799,7 +19032,7 @@ function isTicketBranch(name, base, stems) {
18799
19032
  }
18800
19033
  function sweepCandidatesExist(cwd, base) {
18801
19034
  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;
19035
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename12(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
18803
19036
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
18804
19037
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
18805
19038
  }
@@ -18850,14 +19083,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
18850
19083
  }
18851
19084
 
18852
19085
  // 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";
19086
+ import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync17, statSync as statSync10 } from "fs";
19087
+ import { join as join39 } from "path";
18855
19088
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
18856
19089
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
18857
- const dir = join38(stateDir, "streams");
19090
+ const dir = join39(stateDir, "streams");
18858
19091
  if (!existsSync38(dir)) return [];
18859
19092
  const cutoff = now - retentionDays * DAY_MS2;
18860
- return readdirSync15(dir).map((name) => join38(dir, name)).filter((path) => {
19093
+ return readdirSync15(dir).map((name) => join39(dir, name)).filter((path) => {
18861
19094
  try {
18862
19095
  return statSync10(path).mtimeMs < cutoff;
18863
19096
  } catch {
@@ -18873,7 +19106,7 @@ function applyStreamSweep(paths, dryRun, log) {
18873
19106
  continue;
18874
19107
  }
18875
19108
  try {
18876
- rmSync16(path, { force: true });
19109
+ rmSync17(path, { force: true });
18877
19110
  removed++;
18878
19111
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
18879
19112
  } catch (e) {
@@ -18885,7 +19118,7 @@ function applyStreamSweep(paths, dryRun, log) {
18885
19118
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
18886
19119
  if (!existsSync38(stateDir)) return [];
18887
19120
  const cutoff = now - retentionDays * DAY_MS2;
18888
- return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join38(stateDir, name)).filter((path) => {
19121
+ return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join39(stateDir, name)).filter((path) => {
18889
19122
  try {
18890
19123
  return statSync10(path).mtimeMs < cutoff;
18891
19124
  } catch {
@@ -18895,12 +19128,12 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
18895
19128
  }
18896
19129
 
18897
19130
  // 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";
19131
+ import { realpathSync as realpathSync3, readFileSync as readFileSync39, existsSync as existsSync45, mkdirSync as mkdirSync29, writeFileSync as writeFileSync32, rmSync as rmSync22, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync14, readdirSync as readdirSync20 } from "fs";
18899
19132
  import { createInterface } from "readline/promises";
18900
19133
 
18901
19134
  // 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";
19135
+ import { existsSync as existsSync39, mkdirSync as mkdirSync24, readFileSync as readFileSync34, rmSync as rmSync18, writeFileSync as writeFileSync28 } from "fs";
19136
+ import { join as join40 } from "path";
18904
19137
  var PeriscopeError = class extends Error {
18905
19138
  };
18906
19139
  var LivePeriscopeError = class extends PeriscopeError {
@@ -18910,15 +19143,15 @@ var LivePeriscopeError = class extends PeriscopeError {
18910
19143
  this.data = { refused: "live_periscope_exists", ...live };
18911
19144
  }
18912
19145
  };
18913
- var priorDir = (stateDir) => join39(stateDir, "periscope-priors");
18914
- var priorPath = (stateDir, ticketRecordId) => join39(priorDir(stateDir), `${ticketRecordId}.json`);
19146
+ var priorDir = (stateDir) => join40(stateDir, "periscope-priors");
19147
+ var priorPath = (stateDir, ticketRecordId) => join40(priorDir(stateDir), `${ticketRecordId}.json`);
18915
19148
  function removePeriscopePrior(stateDir, ticketRecordId) {
18916
- rmSync17(priorPath(stateDir, ticketRecordId), { force: true });
19149
+ rmSync18(priorPath(stateDir, ticketRecordId), { force: true });
18917
19150
  }
18918
19151
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
18919
19152
  try {
18920
19153
  mkdirSync24(priorDir(stateDir), { recursive: true });
18921
- writeFileSync27(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
19154
+ writeFileSync28(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
18922
19155
  } catch {
18923
19156
  }
18924
19157
  }
@@ -19322,14 +19555,14 @@ ${tail2(preview.output)}`);
19322
19555
  }
19323
19556
 
19324
19557
  // src/periscope-idle.ts
19325
- import { execFileSync as execFileSync8 } from "child_process";
19558
+ import { execFileSync as execFileSync9 } from "child_process";
19326
19559
  import { statSync as statSync11 } from "fs";
19327
- import { join as join40 } from "path";
19560
+ import { join as join41 } from "path";
19328
19561
  var MAX_SCANNED_FILES = 2e4;
19329
19562
  function newestWorktreeMtime(root) {
19330
19563
  let listing;
19331
19564
  try {
19332
- listing = execFileSync8("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19565
+ listing = execFileSync9("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19333
19566
  cwd: root,
19334
19567
  encoding: "utf8",
19335
19568
  stdio: ["ignore", "pipe", "ignore"],
@@ -19344,7 +19577,7 @@ function newestWorktreeMtime(root) {
19344
19577
  if (!rel) continue;
19345
19578
  if (++scanned > MAX_SCANNED_FILES) break;
19346
19579
  try {
19347
- const m = statSync11(join40(root, rel)).mtimeMs;
19580
+ const m = statSync11(join41(root, rel)).mtimeMs;
19348
19581
  if (newest === null || m > newest) newest = m;
19349
19582
  } catch {
19350
19583
  }
@@ -19419,13 +19652,13 @@ async function runPeriscopeIdlePass(d) {
19419
19652
  import { dirname as dirOf2, resolve as resolvePath } from "path";
19420
19653
 
19421
19654
  // src/ssh-keys.ts
19422
- import { execFileSync as execFileSync9 } from "child_process";
19655
+ import { execFileSync as execFileSync10 } from "child_process";
19423
19656
  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";
19657
+ import { dirname as dirname22, join as join42 } from "path";
19425
19658
  var SshKeyError = class extends Error {
19426
19659
  };
19427
19660
  function shipSshKeyPathFor(stateDir) {
19428
- return join41(stateDir, "ssh", "id_ed25519");
19661
+ return join42(stateDir, "ssh", "id_ed25519");
19429
19662
  }
19430
19663
  function ensureShipSshKeypair(stateDir) {
19431
19664
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -19433,7 +19666,7 @@ function ensureShipSshKeypair(stateDir) {
19433
19666
  if (!existsSync40(privateKeyPath)) {
19434
19667
  mkdirSync25(dirname22(privateKeyPath), { recursive: true });
19435
19668
  try {
19436
- execFileSync9("ssh-keygen", [
19669
+ execFileSync10("ssh-keygen", [
19437
19670
  "-t",
19438
19671
  "ed25519",
19439
19672
  "-N",
@@ -19459,7 +19692,7 @@ function ensureShipSshKeypair(stateDir) {
19459
19692
  }
19460
19693
  function sshKeygenAvailable() {
19461
19694
  try {
19462
- execFileSync9("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
19695
+ execFileSync10("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
19463
19696
  return true;
19464
19697
  } catch (e) {
19465
19698
  return e.code !== "ENOENT";
@@ -19616,10 +19849,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
19616
19849
 
19617
19850
  // src/passenger-sync-daemon.ts
19618
19851
  import { createServer as createHttpServer } from "http";
19619
- import { spawn as spawn6 } from "child_process";
19852
+ import { spawn as spawn7 } from "child_process";
19620
19853
  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";
19854
+ import { existsSync as existsSync41, mkdirSync as mkdirSync26, readFileSync as readFileSync36, writeFileSync as writeFileSync29, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
19855
+ import { join as join43 } from "path";
19623
19856
 
19624
19857
  // src/secret-compare.ts
19625
19858
  import { timingSafeEqual } from "crypto";
@@ -19703,12 +19936,12 @@ function runSyncDaemonFromEnv(env = process.env) {
19703
19936
  return server;
19704
19937
  }
19705
19938
  var defaultSpawn2 = (cmd, args, env) => {
19706
- const child = spawn6(cmd, args, { detached: true, stdio: "ignore", env });
19939
+ const child = spawn7(cmd, args, { detached: true, stdio: "ignore", env });
19707
19940
  child.unref();
19708
19941
  return child;
19709
19942
  };
19710
19943
  function syncDaemonStatePath(stateDir, workspaceId) {
19711
- return join42(stateDir, "passengers", `${workspaceId}-sync.json`);
19944
+ return join43(stateDir, "passengers", `${workspaceId}-sync.json`);
19712
19945
  }
19713
19946
  function readPersistedSyncDaemon(stateDir, workspaceId) {
19714
19947
  const path = syncDaemonStatePath(stateDir, workspaceId);
@@ -19721,8 +19954,8 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
19721
19954
  }
19722
19955
  function writePersistedSyncDaemon(stateDir, state) {
19723
19956
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
19724
- mkdirSync26(join42(stateDir, "passengers"), { recursive: true });
19725
- writeFileSync28(path, `${JSON.stringify(state, null, 2)}
19957
+ mkdirSync26(join43(stateDir, "passengers"), { recursive: true });
19958
+ writeFileSync29(path, `${JSON.stringify(state, null, 2)}
19726
19959
  `);
19727
19960
  }
19728
19961
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -19742,7 +19975,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
19742
19975
  }
19743
19976
  }
19744
19977
  function readAllPersistedSyncWorkspaceIds(stateDir) {
19745
- const dir = join42(stateDir, "passengers");
19978
+ const dir = join43(stateDir, "passengers");
19746
19979
  if (!existsSync41(dir)) return [];
19747
19980
  const suffix = "-sync.json";
19748
19981
  return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -19831,8 +20064,8 @@ function describeCause(err3) {
19831
20064
  }
19832
20065
 
19833
20066
  // 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";
20067
+ import { existsSync as existsSync42, mkdirSync as mkdirSync27, readFileSync as readFileSync37, readdirSync as readdirSync17, rmSync as rmSync19, statSync as statSync12, writeFileSync as writeFileSync30 } from "fs";
20068
+ import { join as join44 } from "path";
19836
20069
  import { randomBytes as randomBytes2 } from "crypto";
19837
20070
  var DEFAULT_FLOOR_MS = 5e3;
19838
20071
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -19920,7 +20153,7 @@ function latestMtime(path) {
19920
20153
  if (!stat2.isDirectory()) return 0;
19921
20154
  let latest = stat2.mtimeMs;
19922
20155
  for (const entry of readdirSync17(path, { withFileTypes: true })) {
19923
- latest = Math.max(latest, latestMtime(join43(path, entry.name)));
20156
+ latest = Math.max(latest, latestMtime(join44(path, entry.name)));
19924
20157
  }
19925
20158
  return latest;
19926
20159
  }
@@ -19932,7 +20165,7 @@ function makeStaleChecker(paths, opts = {}) {
19932
20165
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync42(opts.reloadSentinel);
19933
20166
  }
19934
20167
  function reloadSentinelFor(stateDir) {
19935
- return join43(stateDir, "reload-requested");
20168
+ return join44(stateDir, "reload-requested");
19936
20169
  }
19937
20170
  function readReloadRequestedAt(stateDir) {
19938
20171
  try {
@@ -19942,12 +20175,12 @@ function readReloadRequestedAt(stateDir) {
19942
20175
  }
19943
20176
  }
19944
20177
  function daemonStampFor(stateDir) {
19945
- return join43(stateDir, "daemon.started");
20178
+ return join44(stateDir, "daemon.started");
19946
20179
  }
19947
20180
  function noteDaemonStarted(stateDir, now = Date.now()) {
19948
20181
  mkdirSync27(stateDir, { recursive: true });
19949
- rmSync18(reloadSentinelFor(stateDir), { force: true });
19950
- writeFileSync29(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
20182
+ rmSync19(reloadSentinelFor(stateDir), { force: true });
20183
+ writeFileSync30(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
19951
20184
  `);
19952
20185
  }
19953
20186
  function readDaemonStarted(stateDir) {
@@ -20017,7 +20250,7 @@ async function runOnePassUnguarded(o) {
20017
20250
  await hydrateApiKeys([o.route], o.getSessionStore);
20018
20251
  const emit = o.newEmitter();
20019
20252
  const decide = o.decide ?? decideCycle;
20020
- const spawn9 = o.spawnRoleAgent ?? runRoleAgent;
20253
+ const spawn10 = o.spawnRoleAgent ?? runRoleAgent;
20021
20254
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
20022
20255
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
20023
20256
  if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
@@ -20121,7 +20354,7 @@ async function runOnePassUnguarded(o) {
20121
20354
  settle = resolve7;
20122
20355
  });
20123
20356
  const ready = (tookTicket2) => settle(tookTicket2);
20124
- const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
20357
+ const run2 = spawn10(role, decision, ctx, { ready }).then(() => {
20125
20358
  breaker.recordSuccess(role);
20126
20359
  clearShipAttention(o.state, `role_parked:${role}`);
20127
20360
  clearShipAttention(o.state, workspaceAuthKey(role));
@@ -20516,17 +20749,17 @@ import {
20516
20749
  openSync as openSync5,
20517
20750
  readSync as readSync3,
20518
20751
  readdirSync as readdirSync18,
20519
- rmSync as rmSync19,
20752
+ rmSync as rmSync20,
20520
20753
  statSync as statSync13,
20521
20754
  watch
20522
20755
  } from "fs";
20523
20756
  import { createConnection } from "net";
20524
20757
  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";
20758
+ import { basename as basename13, dirname as dirname23, join as join45 } from "path";
20759
+ import { execFile as execFile2, spawn as spawn9 } from "child_process";
20527
20760
 
20528
20761
  // src/periscope-control.ts
20529
- import { spawn as spawn7 } from "child_process";
20762
+ import { spawn as spawn8 } from "child_process";
20530
20763
  var reply = (status2, body) => ({ status: status2, body });
20531
20764
  var err = (status2, message) => reply(status2, { error: message });
20532
20765
  var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
@@ -20717,7 +20950,7 @@ async function handlePeriscopes(h, q) {
20717
20950
  }
20718
20951
  function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20719
20952
  return new Promise((resolve7) => {
20720
- const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
20953
+ const child = spawn8(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
20721
20954
  let stdout = "";
20722
20955
  let stderr = "";
20723
20956
  let pending = "";
@@ -20751,7 +20984,7 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20751
20984
  // src/control-api.ts
20752
20985
  var PAUSABLE = ["dev", "design", "qa"];
20753
20986
  var CONTROL_SOCKET_NAME = "control.sock";
20754
- var controlSocketFor = (stateDir) => join44(stateDir, CONTROL_SOCKET_NAME);
20987
+ var controlSocketFor = (stateDir) => join45(stateDir, CONTROL_SOCKET_NAME);
20755
20988
  var MAX_SOCKET_PATH = 100;
20756
20989
  var MAX_TAIL_BYTES = 1024 * 1024;
20757
20990
  var MAX_WAIT_SECONDS = 30;
@@ -20838,7 +21071,7 @@ function candidates(live, rotated) {
20838
21071
  const out = [live];
20839
21072
  if (!rotated) return out;
20840
21073
  try {
20841
- for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join44(dirname23(live), n));
21074
+ for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join45(dirname23(live), n));
20842
21075
  } catch {
20843
21076
  }
20844
21077
  return out;
@@ -20961,7 +21194,7 @@ function pauseDoc(state) {
20961
21194
  }
20962
21195
  function describeStream(r) {
20963
21196
  return {
20964
- run: basename12(r.streamPath).replace(/\.jsonl$/, ""),
21197
+ run: basename13(r.streamPath).replace(/\.jsonl$/, ""),
20965
21198
  pid: r.pid,
20966
21199
  ticket: r.ticket,
20967
21200
  role: r.role,
@@ -21038,7 +21271,7 @@ async function route(deps, method, url, req, abort) {
21038
21271
  if (p.startsWith("/streams/") && get2) {
21039
21272
  const run2 = decodeURIComponent(p.slice("/streams/".length));
21040
21273
  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`);
21274
+ const file = join45(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
21042
21275
  if (!existsSync43(file)) return err2(404, "no such run");
21043
21276
  return tailReply(file, url.searchParams, false, abort);
21044
21277
  }
@@ -21101,7 +21334,7 @@ async function startControlServer(deps) {
21101
21334
  log(`control API not started: socket path is too long for a unix socket (${path})`);
21102
21335
  return null;
21103
21336
  }
21104
- if (await socketIsStale(path)) rmSync19(path, { force: true });
21337
+ if (await socketIsStale(path)) rmSync20(path, { force: true });
21105
21338
  else if (existsSync43(path)) {
21106
21339
  log(`control API not started: ${path} is in use by another process`);
21107
21340
  return null;
@@ -21148,7 +21381,7 @@ async function startControlServer(deps) {
21148
21381
  path,
21149
21382
  close: () => new Promise((done) => {
21150
21383
  server.close(() => {
21151
- rmSync19(path, { force: true });
21384
+ rmSync20(path, { force: true });
21152
21385
  done();
21153
21386
  });
21154
21387
  })
@@ -21157,7 +21390,7 @@ async function startControlServer(deps) {
21157
21390
  });
21158
21391
  }
21159
21392
  var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
21160
- function runDetached(commands, spawnFn = spawn8) {
21393
+ function runDetached(commands, spawnFn = spawn9) {
21161
21394
  const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
21162
21395
  const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
21163
21396
  child.on("error", () => {
@@ -21175,7 +21408,7 @@ function schedulerHandler(control, run2 = runDetached) {
21175
21408
  }
21176
21409
  function crewJson(args, timeoutMs = 6e4) {
21177
21410
  return new Promise((resolve7) => {
21178
- execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
21411
+ execFile2(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
21179
21412
  try {
21180
21413
  const v = JSON.parse(String(stdout));
21181
21414
  resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
@@ -21188,24 +21421,24 @@ function crewJson(args, timeoutMs = 6e4) {
21188
21421
 
21189
21422
  // src/periscope-mcp.ts
21190
21423
  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";
21424
+ import { basename as basename14, dirname as dirname24, isAbsolute as isAbsolute3, join as join47, relative as relative2, resolve as resolve5, sep } from "path";
21192
21425
  import { z } from "zod";
21193
21426
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21194
21427
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21195
21428
 
21196
21429
  // src/periscope-endpoints.ts
21197
21430
  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`);
21431
+ import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync28, readdirSync as readdirSync19, readFileSync as readFileSync38, rmSync as rmSync21, writeFileSync as writeFileSync31 } from "fs";
21432
+ import { join as join46 } from "path";
21433
+ var secretDir = (stateDir) => join46(stateDir, "periscope-mcp");
21434
+ var secretFile = (stateDir, id) => join46(secretDir(stateDir), `${id}.json`);
21435
+ var endpointDir = (stateDir) => join46(stateDir, "periscope-endpoints");
21436
+ var endpointFile = (stateDir, id) => join46(endpointDir(stateDir), `${id}.json`);
21204
21437
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
21205
21438
  mkdirSync28(secretDir(stateDir), { recursive: true, mode: 448 });
21206
21439
  const secret = randomBytes3(32).toString("base64url");
21207
21440
  const file = secretFile(stateDir, ticketRecordId);
21208
- writeFileSync30(file, `${JSON.stringify({ secret })}
21441
+ writeFileSync31(file, `${JSON.stringify({ secret })}
21209
21442
  `, { mode: 384 });
21210
21443
  chmodSync5(file, 384);
21211
21444
  return secret;
@@ -21219,7 +21452,7 @@ function readPeriscopeSecret(stateDir, ticketRecordId) {
21219
21452
  }
21220
21453
  }
21221
21454
  function removePeriscopeSecret(stateDir, ticketRecordId) {
21222
- rmSync20(secretFile(stateDir, ticketRecordId), { force: true });
21455
+ rmSync21(secretFile(stateDir, ticketRecordId), { force: true });
21223
21456
  }
21224
21457
  function readPeriscopeEndpoint(stateDir, ticketRecordId) {
21225
21458
  try {
@@ -21230,11 +21463,11 @@ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
21230
21463
  }
21231
21464
  function writePeriscopeEndpoint(stateDir, rec) {
21232
21465
  mkdirSync28(endpointDir(stateDir), { recursive: true });
21233
- writeFileSync30(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
21466
+ writeFileSync31(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
21234
21467
  `);
21235
21468
  }
21236
21469
  function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
21237
- rmSync20(endpointFile(stateDir, ticketRecordId), { force: true });
21470
+ rmSync21(endpointFile(stateDir, ticketRecordId), { force: true });
21238
21471
  }
21239
21472
  function listPeriscopeEndpoints(stateDir) {
21240
21473
  const dir = endpointDir(stateDir);
@@ -21331,7 +21564,7 @@ async function realAncestor(p) {
21331
21564
  }
21332
21565
  const parent = dirname24(cur);
21333
21566
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
21334
- tail3.push(basename13(cur));
21567
+ tail3.push(basename14(cur));
21335
21568
  cur = parent;
21336
21569
  }
21337
21570
  }
@@ -21341,13 +21574,13 @@ async function resolveInWorktree(root, requested) {
21341
21574
  let abs;
21342
21575
  if (isAbsolute3(requested)) {
21343
21576
  abs = resolve5(requested);
21344
- if (isInside(root, abs)) abs = join46(realRoot, relative2(root, abs));
21577
+ if (isInside(root, abs)) abs = join47(realRoot, relative2(root, abs));
21345
21578
  } else {
21346
21579
  abs = resolve5(realRoot, requested);
21347
21580
  }
21348
21581
  if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21349
21582
  const { real, rest } = await realAncestor(abs);
21350
- const final = rest ? join46(real, rest) : real;
21583
+ const final = rest ? join47(real, rest) : real;
21351
21584
  if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21352
21585
  if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
21353
21586
  return final;
@@ -21415,7 +21648,7 @@ function createPeriscopeMcpServer(d, sc) {
21415
21648
  const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
21416
21649
  const lines2 = await Promise.all(entries.map(async (e) => {
21417
21650
  if (e.isDirectory()) return `[DIR] ${e.name}`;
21418
- const size = (await lstat(join46(abs, e.name))).size;
21651
+ const size = (await lstat(join47(abs, e.name))).size;
21419
21652
  return `[FILE] ${e.name} (${size} bytes)`;
21420
21653
  }));
21421
21654
  return text(lines2.join("\n") || "(empty)");
@@ -21481,8 +21714,8 @@ function createPeriscopeMcpServer(d, sc) {
21481
21714
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
21482
21715
  }, async ({ path }) => {
21483
21716
  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);
21717
+ const abs = join47(parent, basename14(path));
21718
+ if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
21486
21719
  const st = await lstat(abs).catch(() => void 0);
21487
21720
  if (!st) return text(`${path} does not exist`, true);
21488
21721
  if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
@@ -22181,7 +22414,7 @@ if (command === "passenger-sync-daemon") {
22181
22414
  stateDir: cfg.ship.stateDir,
22182
22415
  configFile: cfg.configFile,
22183
22416
  readFile: (p) => readFileSync39(p, "utf8"),
22184
- writeFile: (p, t) => writeFileSync31(p, t),
22417
+ writeFile: (p, t) => writeFileSync32(p, t),
22185
22418
  stdin: () => readFileSync39(0, "utf8"),
22186
22419
  env: process.env
22187
22420
  }
@@ -22465,7 +22698,7 @@ if (command === "passenger-sync-daemon") {
22465
22698
  }
22466
22699
  if (host === "linux" && hasSystemd) {
22467
22700
  const label = labelFor(CREW_HOME, "run");
22468
- const staleTimer = join47(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22701
+ const staleTimer = join48(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22469
22702
  if (existsSync45(staleTimer)) {
22470
22703
  process.stdout.write(
22471
22704
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -22528,7 +22761,7 @@ if (command === "passenger-sync-daemon") {
22528
22761
  for (const a of plan.actions) {
22529
22762
  if (a.kind === "enable") {
22530
22763
  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));
22764
+ writeFileSync32(cfg.configFile, planEnable(readFileSync39(cfg.configFile, "utf8"), a.route));
22532
22765
  enabledSound.add(a.route);
22533
22766
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
22534
22767
  } else {
@@ -23651,7 +23884,7 @@ ${cu.tail.trimEnd()}
23651
23884
  process.exit(1);
23652
23885
  }
23653
23886
  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")];
23887
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join48(CREW_HOME, "src"), join48(CREW_HOME, "dist"), join48(CREW_HOME, "bin")];
23655
23888
  const configPaths = daemonConfigPaths(cfg);
23656
23889
  watchPaths.push(...configPaths);
23657
23890
  noteDaemonStarted(cfg.ship.stateDir);
@@ -23950,7 +24183,7 @@ ${cu.tail.trimEnd()}
23950
24183
  const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync39(resolvedPath, "utf8")) : {};
23951
24184
  raw.agentSkills = result.agentSkills;
23952
24185
  mkdirSync29(dirOf2(resolvedPath), { recursive: true });
23953
- writeFileSync31(resolvedPath, `${JSON.stringify(raw, null, 2)}
24186
+ writeFileSync32(resolvedPath, `${JSON.stringify(raw, null, 2)}
23954
24187
  `);
23955
24188
  }
23956
24189
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -23973,7 +24206,7 @@ ${cu.tail.trimEnd()}
23973
24206
  throw e;
23974
24207
  }
23975
24208
  const oldName = cfg.ship.name;
23976
- writeFileSync31(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), newName));
24209
+ writeFileSync32(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), newName));
23977
24210
  cfg.ship.name = newName;
23978
24211
  let renameMachineId;
23979
24212
  try {
@@ -24102,19 +24335,19 @@ ${cu.tail.trimEnd()}
24102
24335
  let cleared;
24103
24336
  if (clear2) {
24104
24337
  const plan = clearRouteHooks(configText, a.route, a.name);
24105
- if (plan.removed.length > 0) writeFileSync31(cfg.configFile, plan.text);
24338
+ if (plan.removed.length > 0) writeFileSync32(cfg.configFile, plan.text);
24106
24339
  cleared = { file: cfg.configFile, removed: plan.removed };
24107
24340
  }
24108
24341
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
24109
24342
  if (write === "route") {
24110
24343
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
24111
- writeFileSync31(cfg.configFile, plan.text);
24344
+ writeFileSync32(cfg.configFile, plan.text);
24112
24345
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
24113
24346
  } else if (write === "repo") {
24114
24347
  const have = existingRepoConfig(files);
24115
24348
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
24116
24349
  const file = resolvePath(dir, ".crew.yaml");
24117
- writeFileSync31(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24350
+ writeFileSync32(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24118
24351
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
24119
24352
  }
24120
24353
  if (flag("json")) {
@@ -24281,7 +24514,7 @@ ${cu.tail.trimEnd()}
24281
24514
  throw new ReposError(`${e.message}${authHint}
24282
24515
  crew.yaml was not changed`);
24283
24516
  }
24284
- writeFileSync31(cfg.configFile, plan2.text);
24517
+ writeFileSync32(cfg.configFile, plan2.text);
24285
24518
  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
24519
  `);
24287
24520
  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}`);
@@ -24344,11 +24577,11 @@ ${cu.tail.trimEnd()}
24344
24577
  const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
24345
24578
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
24346
24579
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
24347
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
24580
+ writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
24348
24581
  `);
24349
24582
  }
24350
24583
  }
24351
- writeFileSync31(cfg.configFile, plan.text);
24584
+ writeFileSync32(cfg.configFile, plan.text);
24352
24585
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
24353
24586
  `);
24354
24587
  } catch (e) {
@@ -24717,7 +24950,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24717
24950
  process.exit(2);
24718
24951
  }
24719
24952
  if (found.shipNameAdopted && !dryRun) {
24720
- writeFileSync31(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), found.shipNameAdopted));
24953
+ writeFileSync32(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), found.shipNameAdopted));
24721
24954
  cfg.ship.name = found.shipNameAdopted;
24722
24955
  }
24723
24956
  if (found.shipRow) {
@@ -24896,7 +25129,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24896
25129
  mkdirSync29(dirname25(resolvedPath), { recursive: true });
24897
25130
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
24898
25131
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
24899
- writeFileSync31(tmpPath, `${JSON.stringify(resolved, null, 2)}
25132
+ writeFileSync32(tmpPath, `${JSON.stringify(resolved, null, 2)}
24900
25133
  `);
24901
25134
  renameSync5(tmpPath, resolvedPath);
24902
25135
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -24943,7 +25176,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24943
25176
  `);
24944
25177
  } else {
24945
25178
  mkdirSync29(dirname25(apiKeyFile), { recursive: true });
24946
- writeFileSync31(apiKeyFile, `${apiKeyVar}=${apiKey}
25179
+ writeFileSync32(apiKeyFile, `${apiKeyVar}=${apiKey}
24947
25180
  `, { mode: 384 });
24948
25181
  process.stderr.write(`API key written to ${apiKeyFile}
24949
25182
  `);
@@ -25773,10 +26006,10 @@ ${where.join("\n")}`);
25773
26006
  },
25774
26007
  prepare: async (path) => {
25775
26008
  for (const f of repoP.config.worktrees.copy) {
25776
- const src = join47(repoP.dir, f);
26009
+ const src = join48(repoP.dir, f);
25777
26010
  if (!existsSync45(src)) continue;
25778
- mkdirSync29(dirOf2(join47(path, f)), { recursive: true });
25779
- copyFileSync2(src, join47(path, f));
26011
+ mkdirSync29(dirOf2(join48(path, f)), { recursive: true });
26012
+ copyFileSync2(src, join48(path, f));
25780
26013
  }
25781
26014
  const setup = repoP.config.hooks.setup;
25782
26015
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -26125,7 +26358,7 @@ ${where.join("\n")}`);
26125
26358
  break;
26126
26359
  }
26127
26360
  mkdirSync29(cfg.ship.stateDir, { recursive: true });
26128
- writeFileSync31(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26361
+ writeFileSync32(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26129
26362
  `);
26130
26363
  process.stdout.write(
26131
26364
  "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"
@@ -26230,7 +26463,7 @@ ${where.join("\n")}`);
26230
26463
  }
26231
26464
  if (configured2.some((r) => r.dir === dir)) {
26232
26465
  try {
26233
- writeFileSync31(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26466
+ writeFileSync32(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26234
26467
  } catch (e) {
26235
26468
  if (e instanceof ReposError) fail(e.message);
26236
26469
  throw e;
@@ -26242,16 +26475,16 @@ ${where.join("\n")}`);
26242
26475
  const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26243
26476
  if (raw.repoNames) delete raw.repoNames[row.id];
26244
26477
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
26245
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
26478
+ writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
26246
26479
  `);
26247
26480
  }
26248
26481
  }
26249
- if (existsSync45(join47(dir, ".git")) && basename14(dir) === SAMPLE_REPO) rmSync21(dir, { recursive: true, force: true });
26482
+ if (existsSync45(join48(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync22(dir, { recursive: true, force: true });
26250
26483
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
26251
26484
  break;
26252
26485
  }
26253
26486
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
26254
- const fresh = !existsSync45(join47(dir, ".git"));
26487
+ const fresh = !existsSync45(join48(dir, ".git"));
26255
26488
  if (existsSync45(dir) && fresh && readdirSync20(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
26256
26489
  if (dryRun) {
26257
26490
  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`);
@@ -26259,8 +26492,8 @@ ${where.join("\n")}`);
26259
26492
  }
26260
26493
  if (fresh) {
26261
26494
  mkdirSync29(dir, { recursive: true });
26262
- writeFileSync31(join47(dir, "README.md"), SAMPLE_README);
26263
- writeFileSync31(join47(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
26495
+ writeFileSync32(join48(dir, "README.md"), SAMPLE_README);
26496
+ writeFileSync32(join48(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
26264
26497
  const g = (...a) => {
26265
26498
  const r = spawnSync10("git", a, { cwd: dir, encoding: "utf8" });
26266
26499
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -26282,13 +26515,13 @@ ${where.join("\n")}`);
26282
26515
  const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26283
26516
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
26284
26517
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
26285
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
26518
+ writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
26286
26519
  `);
26287
26520
  }
26288
26521
  }
26289
26522
  if (!configured2.some((r) => r.dir === dir)) {
26290
26523
  try {
26291
- writeFileSync31(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
26524
+ writeFileSync32(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
26292
26525
  } catch (e) {
26293
26526
  if (e instanceof ReposError) fail(e.message);
26294
26527
  throw e;
@@ -26523,7 +26756,7 @@ ${where.join("\n")}`);
26523
26756
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
26524
26757
  // config-provenance display; this is the field that actually
26525
26758
  // 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>`),
26759
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
26527
26760
  worktreeParent: resolvePath(r.dir, "..")
26528
26761
  })),
26529
26762
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -26781,7 +27014,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
26781
27014
  );
26782
27015
  for (const r of await resolvedRepos(route2)) {
26783
27016
  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)}`);
27017
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
26785
27018
  process.stdout.write(
26786
27019
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
26787
27020
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -27103,6 +27336,13 @@ ${foreign.join("\n")}
27103
27336
  `);
27104
27337
  }
27105
27338
  }
27339
+ if (cfg.ship.runner === "sandbox") {
27340
+ const sb = checkSandboxRunner(cfg.ship.sandbox);
27341
+ process.stdout.write(`sandbox runner: ${sb.ok ? `OK (${sb.detail})` : `NOT READY \u2014 ${sb.detail}`}
27342
+ `);
27343
+ } else {
27344
+ process.stdout.write("sandbox runner: off (ship.runner is host)\n");
27345
+ }
27106
27346
  const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
27107
27347
  if (passengerRoutes.length === 0) {
27108
27348
  process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");