@tablation/crew 0.102.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 +592 -319
  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 } : {} };
@@ -12387,7 +12620,17 @@ async function runProvision(o, deps, write, human) {
12387
12620
  emit("problem", message, data);
12388
12621
  return { code, summary: { ...summary, ok: false, state: "failed" } };
12389
12622
  };
12390
- if (!o.key) return fail(PROVISION_EXIT.problem, "no connect key: pass --key or set CREW_CONNECT_KEY", { stage: "input" });
12623
+ const keyFor = (route2) => {
12624
+ const ws = route2.split("/")[0].toLowerCase();
12625
+ const hit = Object.entries(o.keys ?? {}).find(([k, v]) => k.toLowerCase() === ws && v);
12626
+ return hit?.[1] || o.key || void 0;
12627
+ };
12628
+ for (const route2 of o.routes) {
12629
+ if (!keyFor(route2)) {
12630
+ return fail(PROVISION_EXIT.problem, `no connect key for ${route2}: pass --key or set CREW_CONNECT_KEY_${envKey(route2.split("/")[0])} or CREW_CONNECT_KEY`, { stage: "input" });
12631
+ }
12632
+ }
12633
+ if (o.routes.length === 0 && !o.key) return fail(PROVISION_EXIT.problem, "no connect key: pass --key or set CREW_CONNECT_KEY", { stage: "input" });
12391
12634
  if (o.routes.length === 0) {
12392
12635
  return fail(PROVISION_EXIT.problem, "no routes to serve: pass --route WORKSPACE[/PROJECT] (repeatable) or set CREW_PROVISION_ROUTES", { stage: "input" });
12393
12636
  }
@@ -12413,7 +12656,7 @@ async function runProvision(o, deps, write, human) {
12413
12656
  ...o.dryRun ? ["--dry-run"] : [],
12414
12657
  ...o.choices.flatMap((c) => ["--choose", c])
12415
12658
  ];
12416
- const res = deps.crew(args, { env: { CREW_CONNECT_KEY: o.key } });
12659
+ const res = deps.crew(args, { env: { CREW_CONNECT_KEY: keyFor(route2) } });
12417
12660
  const events = parseEvents(res.stdout);
12418
12661
  for (const e of events) if (o.json) write(`${JSON.stringify({ ...e, phase: "connect" })}
12419
12662
  `);
@@ -12503,6 +12746,15 @@ function tokensFromEnv(hosts, env) {
12503
12746
  }
12504
12747
  return out;
12505
12748
  }
12749
+ function keysFromEnv(routes, env) {
12750
+ const out = {};
12751
+ for (const r of routes) {
12752
+ const ws = r.split("/")[0];
12753
+ const v = env[`CREW_CONNECT_KEY_${envKey(ws)}`]?.trim();
12754
+ if (v) out[ws] = v;
12755
+ }
12756
+ return out;
12757
+ }
12506
12758
  function repeated(argv2, name) {
12507
12759
  const out = [];
12508
12760
  for (let i = 0; i < argv2.length; i++) {
@@ -12523,9 +12775,11 @@ function provisionOptionsFrom(argv2, env) {
12523
12775
  const forgeFlags = repeated(argv2, "forge-host");
12524
12776
  const tokenAvailable = !!env.CREW_FORGE_TOKEN?.trim() || Object.keys(env).some((k) => k.startsWith("CREW_FORGE_TOKEN_") && env[k]?.trim());
12525
12777
  const forgeHosts = (forgeFlags.length ? forgeFlags : tokenAvailable ? splitList(env.CREW_FORGE_HOSTS ?? "github.com") : []).map((h) => h.toLowerCase());
12778
+ const routeList = routes.length ? routes : splitList(env.CREW_PROVISION_ROUTES);
12526
12779
  return {
12527
12780
  key: one("key") ?? env.CREW_CONNECT_KEY,
12528
- routes: routes.length ? routes : splitList(env.CREW_PROVISION_ROUTES),
12781
+ keys: keysFromEnv(routeList, env),
12782
+ routes: routeList,
12529
12783
  shipName: one("ship-name") ?? env.CREW_SHIP_NAME,
12530
12784
  shipKind: one("ship-kind"),
12531
12785
  instanceClass: one("instance-class"),
@@ -12546,6 +12800,7 @@ function realProvisionDeps(crewHome) {
12546
12800
  const [bin, ...pre] = crewInvocation(crewHome);
12547
12801
  const env = { ...process.env, ...o.env };
12548
12802
  delete env.CREW_FORGE_TOKEN;
12803
+ for (const k of Object.keys(env)) if (k.startsWith("CREW_CONNECT_KEY_")) delete env[k];
12549
12804
  const res = spawnSync7(bin, [...pre, ...args], { cwd: crewHome, encoding: "utf8", env, input: o.input, maxBuffer: 64 * 1024 * 1024 });
12550
12805
  return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: `${res.stderr ?? ""}${res.error ? String(res.error) : ""}` };
12551
12806
  };
@@ -12565,7 +12820,7 @@ function realProvisionDeps(crewHome) {
12565
12820
  const entry = routes?.items?.find((r) => r.get("route") === route2);
12566
12821
  if (!entry) throw new Error(`route ${route2} is not in ${file}`);
12567
12822
  if (entry.get("enabled") === true) return false;
12568
- writeFileSync19(file, planEnable(text2, route2));
12823
+ writeFileSync20(file, planEnable(text2, route2));
12569
12824
  return true;
12570
12825
  },
12571
12826
  installState() {
@@ -12612,7 +12867,7 @@ function realProvisionDeps(crewHome) {
12612
12867
  const doc = parseDocument3(readFileSync26(file, "utf8"));
12613
12868
  if (doc.getIn(["ship", key]) === value2) return false;
12614
12869
  doc.setIn(["ship", key], value2);
12615
- writeFileSync19(file, doc.toString());
12870
+ writeFileSync20(file, doc.toString());
12616
12871
  return true;
12617
12872
  },
12618
12873
  storedToken(host) {
@@ -12628,12 +12883,12 @@ host=${host}
12628
12883
  }
12629
12884
 
12630
12885
  // src/tunnel.ts
12631
- import { spawn as spawn4 } from "child_process";
12632
- 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";
12633
12888
  import { randomInt } from "crypto";
12634
- import { join as join31 } from "path";
12889
+ import { join as join32 } from "path";
12635
12890
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
12636
- var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
12891
+ var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
12637
12892
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
12638
12893
  var SLUG_LENGTH = 12;
12639
12894
  function generateTunnelSlug() {
@@ -12703,7 +12958,7 @@ function startTunnel(opts) {
12703
12958
  };
12704
12959
  }
12705
12960
  function tunnelStatePath(stateDir, workspaceId) {
12706
- return join31(stateDir, "passengers", `${workspaceId}.json`);
12961
+ return join32(stateDir, "passengers", `${workspaceId}.json`);
12707
12962
  }
12708
12963
  function readPersistedTunnel(stateDir, workspaceId) {
12709
12964
  const path = tunnelStatePath(stateDir, workspaceId);
@@ -12716,8 +12971,8 @@ function readPersistedTunnel(stateDir, workspaceId) {
12716
12971
  }
12717
12972
  function writePersistedTunnel(stateDir, state) {
12718
12973
  const path = tunnelStatePath(stateDir, state.workspaceId);
12719
- mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
12720
- writeFileSync20(path, `${JSON.stringify(state, null, 2)}
12974
+ mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
12975
+ writeFileSync21(path, `${JSON.stringify(state, null, 2)}
12721
12976
  `);
12722
12977
  }
12723
12978
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -12819,7 +13074,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12819
13074
  }
12820
13075
  }
12821
13076
  function readAllPersistedWorkspaceIds(stateDir) {
12822
- const dir = join31(stateDir, "passengers");
13077
+ const dir = join32(stateDir, "passengers");
12823
13078
  if (!existsSync29(dir)) return [];
12824
13079
  return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12825
13080
  }
@@ -12851,6 +13106,7 @@ function realPoolMemberDeps(o) {
12851
13106
  const lines2 = [];
12852
13107
  const outcome = await runProvision({
12853
13108
  key: lease.connectKey,
13109
+ keys: Object.fromEntries((lease.connectKeys ?? []).map((k) => [k.workspace, k.key])),
12854
13110
  routes: lease.routes.map((r) => `${r.workspace}/${r.project}`),
12855
13111
  shipName: lease.shipName,
12856
13112
  shipKind,
@@ -13685,8 +13941,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
13685
13941
  }
13686
13942
 
13687
13943
  // src/poll.ts
13688
- import { writeFileSync as writeFileSync21 } from "fs";
13689
- import { join as join32 } from "path";
13944
+ import { writeFileSync as writeFileSync22 } from "fs";
13945
+ import { join as join33 } from "path";
13690
13946
 
13691
13947
  // src/stalled.ts
13692
13948
  var STALLED_AFTER_CYCLES = 30;
@@ -13993,7 +14249,7 @@ async function decideCycle(o) {
13993
14249
  }
13994
14250
  return decision;
13995
14251
  }
13996
- 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`);
13997
14253
  function otherRunsRemoved(tickets, own) {
13998
14254
  if (!own) return tickets;
13999
14255
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -14071,7 +14327,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
14071
14327
 
14072
14328
  ${body}` : body;
14073
14329
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
14074
- writeFileSync21(path, text2);
14330
+ writeFileSync22(path, text2);
14075
14331
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
14076
14332
  data: { role, bytes: Buffer.byteLength(text2) }
14077
14333
  });
@@ -14179,7 +14435,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
14179
14435
  }
14180
14436
 
14181
14437
  // src/repo-refresh.ts
14182
- 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";
14183
14439
  import { dirname as dirname14 } from "path";
14184
14440
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
14185
14441
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
@@ -14213,7 +14469,7 @@ function persistRepoRows(stateDir, routeName, rows) {
14213
14469
  const raw = JSON.parse(readFileSync28(path, "utf8"));
14214
14470
  applyRepoRows(raw, rows);
14215
14471
  mkdirSync19(dirname14(path), { recursive: true });
14216
- writeFileSync22(path, `${JSON.stringify(raw, null, 2)}
14472
+ writeFileSync23(path, `${JSON.stringify(raw, null, 2)}
14217
14473
  `);
14218
14474
  }
14219
14475
  async function refreshUnknownRepos(o) {
@@ -14663,8 +14919,8 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
14663
14919
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
14664
14920
 
14665
14921
  // src/connect-wizard.ts
14666
- import { delimiter, dirname as dirname15, join as join33 } from "path";
14667
- 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";
14668
14924
  import { stringify as stringify2 } from "yaml";
14669
14925
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
14670
14926
  function shouldRunWizard(opts) {
@@ -14686,7 +14942,7 @@ function defaultIsExecutable(p) {
14686
14942
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
14687
14943
  for (const dir of pathEnv.split(delimiter)) {
14688
14944
  if (!dir) continue;
14689
- const candidate = join33(dir, name);
14945
+ const candidate = join34(dir, name);
14690
14946
  if (isExecutable(candidate)) return candidate;
14691
14947
  }
14692
14948
  return void 0;
@@ -14749,7 +15005,7 @@ ${routeBlock}`;
14749
15005
  function writeNewConfig(path, text2) {
14750
15006
  mkdirSync20(dirname15(path), { recursive: true });
14751
15007
  try {
14752
- writeFileSync23(path, text2, { mode: 384, flag: "wx" });
15008
+ writeFileSync24(path, text2, { mode: 384, flag: "wx" });
14753
15009
  return true;
14754
15010
  } catch (e) {
14755
15011
  if (e.code === "EEXIST") return false;
@@ -14783,7 +15039,7 @@ Wrote ${o.configPath}.
14783
15039
  // src/connect-json.ts
14784
15040
  import { accessSync as accessSync4, constants as constants4 } from "fs";
14785
15041
  import { homedir as homedir10 } from "os";
14786
- import { dirname as dirname16, join as join34 } from "path";
15042
+ import { dirname as dirname16, join as join35 } from "path";
14787
15043
  import { delimiter as delimiter2 } from "path";
14788
15044
  var CHOICE_EXIT_CODE = 3;
14789
15045
  function createEmitter(json2, write) {
@@ -14822,12 +15078,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
14822
15078
  const dirs = [
14823
15079
  ...pathEnv.split(delimiter2),
14824
15080
  ...(extraPath ?? "").split(delimiter2),
14825
- join34(home, ".local/bin"),
14826
- join34(home, ".claude/local"),
15081
+ join35(home, ".local/bin"),
15082
+ join35(home, ".claude/local"),
14827
15083
  "/opt/homebrew/bin",
14828
15084
  "/usr/local/bin",
14829
- join34(home, ".npm-global/bin"),
14830
- join34(home, ".volta/bin")
15085
+ join35(home, ".npm-global/bin"),
15086
+ join35(home, ".volta/bin")
14831
15087
  ];
14832
15088
  return [...new Set(dirs.filter(Boolean))];
14833
15089
  }
@@ -14863,9 +15119,9 @@ function flagWizardAnswers(f, env) {
14863
15119
  }
14864
15120
 
14865
15121
  // src/release-run.ts
14866
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync24, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync21, mkdtempSync as mkdtempSync2, rmSync as rmSync15 } from "fs";
14867
- import { dirname as dirname17, join as join35 } from "path";
14868
- 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";
14869
15125
 
14870
15126
  // src/release.ts
14871
15127
  function describeUnplaceable(unplaceable, verifiedStatus) {
@@ -15605,7 +15861,7 @@ async function bump(o, size) {
15605
15861
  }
15606
15862
  return printed;
15607
15863
  }
15608
- 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));
15609
15865
  const first = files[0];
15610
15866
  if (!existsSync33(first)) {
15611
15867
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
@@ -15630,21 +15886,21 @@ async function bump(o, size) {
15630
15886
  o.emit.error(`no version field in ${f}`);
15631
15887
  return null;
15632
15888
  }
15633
- writeFileSync24(f, out);
15889
+ writeFileSync25(f, out);
15634
15890
  }
15635
15891
  return version;
15636
15892
  }
15637
15893
  function writeChangelog(o, version, entries) {
15638
15894
  const file = o.repo.release.changelog;
15639
15895
  if (!file || entries.length === 0) return;
15640
- const path = join35(o.cwd, file);
15896
+ const path = join36(o.cwd, file);
15641
15897
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
15642
15898
  if (o.dryRun) {
15643
15899
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
15644
15900
  return;
15645
15901
  }
15646
15902
  const existing = existsSync33(path) ? readFileSync29(path, "utf8") : "# Changelog\n";
15647
- writeFileSync24(path, insertChangelogSection(existing, section));
15903
+ writeFileSync25(path, insertChangelogSection(existing, section));
15648
15904
  }
15649
15905
  async function confirm(o, expected) {
15650
15906
  if (!o.repo.hooks.released) return void 0;
@@ -15727,7 +15983,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
15727
15983
  run2(cwd, ["worktree", "remove", "--force", dir]);
15728
15984
  } finally {
15729
15985
  try {
15730
- rmSync15(root, { recursive: true, force: true });
15986
+ rmSync16(root, { recursive: true, force: true });
15731
15987
  } finally {
15732
15988
  run2(cwd, ["worktree", "prune"]);
15733
15989
  run2(cwd, ["branch", "-D", branch]);
@@ -15737,8 +15993,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
15737
15993
  function openReleaseCheckout(o) {
15738
15994
  const { remote, base } = o.repo.branch;
15739
15995
  const branch = `crew/release-${Date.now()}`;
15740
- const root = mkdtempSync2(join35(tmpdir3(), "crew-release-"));
15741
- const dir = join35(root, "checkout");
15996
+ const root = mkdtempSync3(join36(tmpdir4(), "crew-release-"));
15997
+ const dir = join36(root, "checkout");
15742
15998
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
15743
15999
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
15744
16000
  try {
@@ -15748,10 +16004,10 @@ function openReleaseCheckout(o) {
15748
16004
  throw e;
15749
16005
  }
15750
16006
  for (const f of o.repo.worktrees.copy) {
15751
- const src = join35(o.cwd, f);
16007
+ const src = join36(o.cwd, f);
15752
16008
  if (!existsSync33(src)) continue;
15753
- mkdirSync21(dirname17(join35(dir, f)), { recursive: true });
15754
- copyFileSync(src, join35(dir, f));
16009
+ mkdirSync21(dirname17(join36(dir, f)), { recursive: true });
16010
+ copyFileSync(src, join36(dir, f));
15755
16011
  }
15756
16012
  return { dir, branch, close };
15757
16013
  }
@@ -15896,7 +16152,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
15896
16152
  let checkout;
15897
16153
  const stranded = recoverStrandedReleaseCheckouts(o.cwd);
15898
16154
  if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
15899
- const swept = sweepStaleReleaseRoots(tmpdir3());
16155
+ const swept = sweepStaleReleaseRoots(tmpdir4());
15900
16156
  if (swept.length > 0) o.emit.emit(scoped(`removed ${swept.length} stale crew-release-* temp dir(s)`), { step: "release" });
15901
16157
  try {
15902
16158
  checkout = openReleaseCheckout(o);
@@ -17069,7 +17325,7 @@ function nextRoles(decision, ran) {
17069
17325
  }
17070
17326
 
17071
17327
  // src/ship-name.ts
17072
- 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";
17073
17329
  import { dirname as dirname18 } from "path";
17074
17330
  import { parseDocument as parseDocument5 } from "yaml";
17075
17331
  var ShipNameError = class extends Error {
@@ -17151,7 +17407,7 @@ function writeSyncedName(resolvedFile, name) {
17151
17407
  raw.shipName = name;
17152
17408
  mkdirSync22(dirname18(resolvedFile), { recursive: true });
17153
17409
  const tmp = `${resolvedFile}.tmp${process.pid}`;
17154
- writeFileSync25(tmp, `${JSON.stringify(raw, null, 2)}
17410
+ writeFileSync26(tmp, `${JSON.stringify(raw, null, 2)}
17155
17411
  `);
17156
17412
  renameSync4(tmp, resolvedFile);
17157
17413
  }
@@ -17183,7 +17439,7 @@ async function reconcileShipName(i) {
17183
17439
  if (result.status !== "ok") return result;
17184
17440
  if (result.action === "adopt" || result.action === "row-wins") {
17185
17441
  const text2 = readFileSync30(i.configFile, "utf8");
17186
- writeFileSync25(i.configFile, setShipNameInYaml(text2, result.name));
17442
+ writeFileSync26(i.configFile, setShipNameInYaml(text2, result.name));
17187
17443
  i.ship.name = result.name;
17188
17444
  if (result.action === "row-wins") {
17189
17445
  i.emit?.warn(
@@ -17842,7 +18098,7 @@ function describeCapability(i) {
17842
18098
  }
17843
18099
 
17844
18100
  // src/agents-sync-run.ts
17845
- 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";
17846
18102
  import { dirname as dirname19 } from "path";
17847
18103
  async function runAgentsSync(target, o) {
17848
18104
  const dryRun = o.dryRun ?? false;
@@ -17881,7 +18137,7 @@ async function runAgentsSync(target, o) {
17881
18137
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
17882
18138
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
17883
18139
  mkdirSync23(dirname19(resolvedPath), { recursive: true });
17884
- writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
18140
+ writeFileSync27(resolvedPath, `${JSON.stringify(raw, null, 2)}
17885
18141
  `);
17886
18142
  if (target.resolved) {
17887
18143
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -17986,11 +18242,11 @@ async function showLogEntry(route2, entryId, opts = {}) {
17986
18242
  // src/repos-check.ts
17987
18243
  import { spawnSync as spawnSync9 } from "child_process";
17988
18244
  import { readFileSync as readFileSync32 } from "fs";
17989
- import { basename as basename9 } from "path";
18245
+ import { basename as basename10 } from "path";
17990
18246
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
17991
18247
  function checkHookSyntax(script, shell) {
17992
18248
  const spec = resolveShell(shell);
17993
- const name = basename9(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18249
+ const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
17994
18250
  let run2;
17995
18251
  if (POSIX_SHELL.test(name)) {
17996
18252
  run2 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
@@ -18009,7 +18265,7 @@ function checkHookSyntax(script, shell) {
18009
18265
  return { status: "error", message };
18010
18266
  }
18011
18267
  function renderSamples(cfg, dir) {
18012
- const checkout = basename9(dir.replace(/[/\\]+$/, ""));
18268
+ const checkout = basename10(dir.replace(/[/\\]+$/, ""));
18013
18269
  const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
18014
18270
  const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
18015
18271
  return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
@@ -18301,7 +18557,7 @@ function existingRepoConfig(f) {
18301
18557
  }
18302
18558
 
18303
18559
  // src/repos-hooks-agent.ts
18304
- import { spawn as spawn5, execFileSync as execFileSync7 } from "child_process";
18560
+ import { spawn as spawn6, execFileSync as execFileSync8 } from "child_process";
18305
18561
  import { realpathSync as realpathSync2 } from "fs";
18306
18562
  import { isAbsolute as isAbsolute2, relative } from "path";
18307
18563
  var AgentSuggestError = class extends Error {
@@ -18404,7 +18660,7 @@ function classifyFailure(o) {
18404
18660
  }
18405
18661
  function porcelain(dir) {
18406
18662
  try {
18407
- 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" });
18408
18664
  } catch {
18409
18665
  return void 0;
18410
18666
  }
@@ -18422,7 +18678,7 @@ async function runAgentSuggest(o) {
18422
18678
  const run2 = await new Promise((resolve7) => {
18423
18679
  let out = "";
18424
18680
  let err3 = "";
18425
- 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"] });
18426
18682
  let settled = false;
18427
18683
  const settle = (r) => {
18428
18684
  if (settled) return;
@@ -18473,7 +18729,7 @@ ${run2.err}` });
18473
18729
  }
18474
18730
 
18475
18731
  // src/sample-ticket.ts
18476
- 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";
18477
18733
  var SAMPLE_REPO = "crew-sample";
18478
18734
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
18479
18735
  var SAMPLE_MARKER = "sample: true";
@@ -18496,8 +18752,8 @@ has something safe to work on: its test and build hooks always pass, it has no
18496
18752
  remote, and \`crew sample-ticket --remove\` deletes it.
18497
18753
  `;
18498
18754
  function sampleDir(existing, reposBase, workspace) {
18499
- if (existing.length > 0) return join36(dirname20(existing[0]), SAMPLE_REPO);
18500
- 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);
18501
18757
  }
18502
18758
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
18503
18759
  function sampleDescription() {
@@ -18542,7 +18798,7 @@ function planSampleTicket(i) {
18542
18798
 
18543
18799
  // src/worktree-sweep.ts
18544
18800
  import { existsSync as existsSync37, readFileSync as readFileSync33, readdirSync as readdirSync14 } from "fs";
18545
- 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";
18546
18802
  function planWorktreeSweep(target, tickets, contract) {
18547
18803
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
18548
18804
  const verified = contract.statuses.verified;
@@ -18583,7 +18839,7 @@ function ticketForWorktree(w, byNumber, contract) {
18583
18839
  const t = num ? byNumber.get(num) : void 0;
18584
18840
  if (t && ticketStems(t).has(stem)) return t;
18585
18841
  }
18586
- const dir = basename11(w.path);
18842
+ const dir = basename12(w.path);
18587
18843
  const m = DIR_NUMBER.exec(dir);
18588
18844
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
18589
18845
  if (!w.branch) {
@@ -18599,7 +18855,7 @@ function worktreeTicketNumbers(target) {
18599
18855
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
18600
18856
  if (n) nums.add(n);
18601
18857
  }
18602
- const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
18858
+ const m = DIR_NUMBER_SUFFIXED.exec(basename12(w.path));
18603
18859
  if (m) nums.add(m[1]);
18604
18860
  }
18605
18861
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
@@ -18607,7 +18863,7 @@ function worktreeTicketNumbers(target) {
18607
18863
  }
18608
18864
  function siblingWorktreeDirs(target) {
18609
18865
  const parent = dirname21(target.dir.replace(/[/\\]+$/, ""));
18610
- const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
18866
+ const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
18611
18867
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
18612
18868
  let names;
18613
18869
  try {
@@ -18619,7 +18875,7 @@ function siblingWorktreeDirs(target) {
18619
18875
  for (const name of names) {
18620
18876
  if (!name.startsWith(stem)) continue;
18621
18877
  const m = DIR_NUMBER_SUFFIXED.exec(name);
18622
- const path = join37(parent, name);
18878
+ const path = join38(parent, name);
18623
18879
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
18624
18880
  }
18625
18881
  return out;
@@ -18632,7 +18888,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
18632
18888
  }
18633
18889
  function warnOrphanedWorktreeDirs(paths, log, held) {
18634
18890
  for (const p of paths) {
18635
- const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
18891
+ const number = DIR_NUMBER_SUFFIXED.exec(basename12(p))?.[1];
18636
18892
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
18637
18893
  if (when) {
18638
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" });
@@ -18776,7 +19032,7 @@ function isTicketBranch(name, base, stems) {
18776
19032
  }
18777
19033
  function sweepCandidatesExist(cwd, base) {
18778
19034
  const linked = worktrees(cwd).slice(1);
18779
- 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;
18780
19036
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
18781
19037
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
18782
19038
  }
@@ -18827,14 +19083,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
18827
19083
  }
18828
19084
 
18829
19085
  // src/stream-sweep.ts
18830
- import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync16, statSync as statSync10 } from "fs";
18831
- 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";
18832
19088
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
18833
19089
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
18834
- const dir = join38(stateDir, "streams");
19090
+ const dir = join39(stateDir, "streams");
18835
19091
  if (!existsSync38(dir)) return [];
18836
19092
  const cutoff = now - retentionDays * DAY_MS2;
18837
- return readdirSync15(dir).map((name) => join38(dir, name)).filter((path) => {
19093
+ return readdirSync15(dir).map((name) => join39(dir, name)).filter((path) => {
18838
19094
  try {
18839
19095
  return statSync10(path).mtimeMs < cutoff;
18840
19096
  } catch {
@@ -18850,7 +19106,7 @@ function applyStreamSweep(paths, dryRun, log) {
18850
19106
  continue;
18851
19107
  }
18852
19108
  try {
18853
- rmSync16(path, { force: true });
19109
+ rmSync17(path, { force: true });
18854
19110
  removed++;
18855
19111
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
18856
19112
  } catch (e) {
@@ -18862,7 +19118,7 @@ function applyStreamSweep(paths, dryRun, log) {
18862
19118
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
18863
19119
  if (!existsSync38(stateDir)) return [];
18864
19120
  const cutoff = now - retentionDays * DAY_MS2;
18865
- 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) => {
18866
19122
  try {
18867
19123
  return statSync10(path).mtimeMs < cutoff;
18868
19124
  } catch {
@@ -18872,12 +19128,12 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
18872
19128
  }
18873
19129
 
18874
19130
  // src/cli.ts
18875
- 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";
18876
19132
  import { createInterface } from "readline/promises";
18877
19133
 
18878
19134
  // src/periscope-cmd.ts
18879
- import { existsSync as existsSync39, mkdirSync as mkdirSync24, readFileSync as readFileSync34, rmSync as rmSync17, writeFileSync as writeFileSync27 } from "fs";
18880
- 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";
18881
19137
  var PeriscopeError = class extends Error {
18882
19138
  };
18883
19139
  var LivePeriscopeError = class extends PeriscopeError {
@@ -18887,15 +19143,15 @@ var LivePeriscopeError = class extends PeriscopeError {
18887
19143
  this.data = { refused: "live_periscope_exists", ...live };
18888
19144
  }
18889
19145
  };
18890
- var priorDir = (stateDir) => join39(stateDir, "periscope-priors");
18891
- 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`);
18892
19148
  function removePeriscopePrior(stateDir, ticketRecordId) {
18893
- rmSync17(priorPath(stateDir, ticketRecordId), { force: true });
19149
+ rmSync18(priorPath(stateDir, ticketRecordId), { force: true });
18894
19150
  }
18895
19151
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
18896
19152
  try {
18897
19153
  mkdirSync24(priorDir(stateDir), { recursive: true });
18898
- writeFileSync27(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
19154
+ writeFileSync28(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
18899
19155
  } catch {
18900
19156
  }
18901
19157
  }
@@ -19299,14 +19555,14 @@ ${tail2(preview.output)}`);
19299
19555
  }
19300
19556
 
19301
19557
  // src/periscope-idle.ts
19302
- import { execFileSync as execFileSync8 } from "child_process";
19558
+ import { execFileSync as execFileSync9 } from "child_process";
19303
19559
  import { statSync as statSync11 } from "fs";
19304
- import { join as join40 } from "path";
19560
+ import { join as join41 } from "path";
19305
19561
  var MAX_SCANNED_FILES = 2e4;
19306
19562
  function newestWorktreeMtime(root) {
19307
19563
  let listing;
19308
19564
  try {
19309
- listing = execFileSync8("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19565
+ listing = execFileSync9("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19310
19566
  cwd: root,
19311
19567
  encoding: "utf8",
19312
19568
  stdio: ["ignore", "pipe", "ignore"],
@@ -19321,7 +19577,7 @@ function newestWorktreeMtime(root) {
19321
19577
  if (!rel) continue;
19322
19578
  if (++scanned > MAX_SCANNED_FILES) break;
19323
19579
  try {
19324
- const m = statSync11(join40(root, rel)).mtimeMs;
19580
+ const m = statSync11(join41(root, rel)).mtimeMs;
19325
19581
  if (newest === null || m > newest) newest = m;
19326
19582
  } catch {
19327
19583
  }
@@ -19396,13 +19652,13 @@ async function runPeriscopeIdlePass(d) {
19396
19652
  import { dirname as dirOf2, resolve as resolvePath } from "path";
19397
19653
 
19398
19654
  // src/ssh-keys.ts
19399
- import { execFileSync as execFileSync9 } from "child_process";
19655
+ import { execFileSync as execFileSync10 } from "child_process";
19400
19656
  import { existsSync as existsSync40, mkdirSync as mkdirSync25, readFileSync as readFileSync35, chmodSync as chmodSync3 } from "fs";
19401
- import { dirname as dirname22, join as join41 } from "path";
19657
+ import { dirname as dirname22, join as join42 } from "path";
19402
19658
  var SshKeyError = class extends Error {
19403
19659
  };
19404
19660
  function shipSshKeyPathFor(stateDir) {
19405
- return join41(stateDir, "ssh", "id_ed25519");
19661
+ return join42(stateDir, "ssh", "id_ed25519");
19406
19662
  }
19407
19663
  function ensureShipSshKeypair(stateDir) {
19408
19664
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -19410,7 +19666,7 @@ function ensureShipSshKeypair(stateDir) {
19410
19666
  if (!existsSync40(privateKeyPath)) {
19411
19667
  mkdirSync25(dirname22(privateKeyPath), { recursive: true });
19412
19668
  try {
19413
- execFileSync9("ssh-keygen", [
19669
+ execFileSync10("ssh-keygen", [
19414
19670
  "-t",
19415
19671
  "ed25519",
19416
19672
  "-N",
@@ -19436,7 +19692,7 @@ function ensureShipSshKeypair(stateDir) {
19436
19692
  }
19437
19693
  function sshKeygenAvailable() {
19438
19694
  try {
19439
- execFileSync9("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
19695
+ execFileSync10("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
19440
19696
  return true;
19441
19697
  } catch (e) {
19442
19698
  return e.code !== "ENOENT";
@@ -19493,6 +19749,8 @@ async function call(api, method, url, body) {
19493
19749
  }
19494
19750
  function bodyFor(d, forPatch) {
19495
19751
  const body = { label: d.label, url: d.url };
19752
+ if (d.controlUrl !== void 0) body.controlUrl = d.controlUrl;
19753
+ if (d.discoverable !== void 0) body.discoverable = d.discoverable;
19496
19754
  if (d.mcpSecret) body.authorizationHeader = `Bearer ${d.mcpSecret}`;
19497
19755
  else if (forPatch) body.authorizationHeader = null;
19498
19756
  return body;
@@ -19541,15 +19799,17 @@ async function probeEndpointAccess(api, workspaceId) {
19541
19799
  }
19542
19800
  }
19543
19801
  var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
19544
- function registrationKey(url, mcpSecret) {
19802
+ function registrationKey(url, mcpSecret, controlUrl) {
19545
19803
  return createHash8("sha256").update(`${url}
19546
- ${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
19804
+ ${mcpSecret ?? ""}
19805
+ ${controlUrl ?? ""}`).digest("hex").slice(0, 16);
19547
19806
  }
19548
19807
  async function reconcileEndpoint(workspaceId, d) {
19549
19808
  const st = readPersistedTunnel(d.stateDir, workspaceId);
19550
19809
  if (!st || st.status !== "connected" || !st.slug) return;
19551
19810
  const url = d.urlFor(st.slug);
19552
- const key = registrationKey(url, st.mcpSecret);
19811
+ const controlUrl = st.controlSocket && d.controlUrlFor ? d.controlUrlFor(st.slug) : null;
19812
+ const key = registrationKey(url, st.mcpSecret, controlUrl);
19553
19813
  if (st.endpointId && st.registeredKey === key) return;
19554
19814
  const api = d.apiFor(workspaceId);
19555
19815
  if (!api) {
@@ -19559,7 +19819,8 @@ async function reconcileEndpoint(workspaceId, d) {
19559
19819
  const out = await registerEndpoint(api, workspaceId, st.endpointId, {
19560
19820
  label: `Ship: ${d.shipName}`,
19561
19821
  url,
19562
- mcpSecret: st.mcpSecret
19822
+ mcpSecret: st.mcpSecret,
19823
+ controlUrl
19563
19824
  });
19564
19825
  if (out.kind === "ok") {
19565
19826
  updatePersistedTunnel(d.stateDir, workspaceId, { endpointId: out.endpointId, registeredKey: key });
@@ -19588,10 +19849,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
19588
19849
 
19589
19850
  // src/passenger-sync-daemon.ts
19590
19851
  import { createServer as createHttpServer } from "http";
19591
- import { spawn as spawn6 } from "child_process";
19852
+ import { spawn as spawn7 } from "child_process";
19592
19853
  import { randomBytes } from "crypto";
19593
- import { existsSync as existsSync41, mkdirSync as mkdirSync26, readFileSync as readFileSync36, writeFileSync as writeFileSync28, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
19594
- 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";
19595
19856
 
19596
19857
  // src/secret-compare.ts
19597
19858
  import { timingSafeEqual } from "crypto";
@@ -19675,12 +19936,12 @@ function runSyncDaemonFromEnv(env = process.env) {
19675
19936
  return server;
19676
19937
  }
19677
19938
  var defaultSpawn2 = (cmd, args, env) => {
19678
- const child = spawn6(cmd, args, { detached: true, stdio: "ignore", env });
19939
+ const child = spawn7(cmd, args, { detached: true, stdio: "ignore", env });
19679
19940
  child.unref();
19680
19941
  return child;
19681
19942
  };
19682
19943
  function syncDaemonStatePath(stateDir, workspaceId) {
19683
- return join42(stateDir, "passengers", `${workspaceId}-sync.json`);
19944
+ return join43(stateDir, "passengers", `${workspaceId}-sync.json`);
19684
19945
  }
19685
19946
  function readPersistedSyncDaemon(stateDir, workspaceId) {
19686
19947
  const path = syncDaemonStatePath(stateDir, workspaceId);
@@ -19693,8 +19954,8 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
19693
19954
  }
19694
19955
  function writePersistedSyncDaemon(stateDir, state) {
19695
19956
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
19696
- mkdirSync26(join42(stateDir, "passengers"), { recursive: true });
19697
- writeFileSync28(path, `${JSON.stringify(state, null, 2)}
19957
+ mkdirSync26(join43(stateDir, "passengers"), { recursive: true });
19958
+ writeFileSync29(path, `${JSON.stringify(state, null, 2)}
19698
19959
  `);
19699
19960
  }
19700
19961
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -19714,7 +19975,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
19714
19975
  }
19715
19976
  }
19716
19977
  function readAllPersistedSyncWorkspaceIds(stateDir) {
19717
- const dir = join42(stateDir, "passengers");
19978
+ const dir = join43(stateDir, "passengers");
19718
19979
  if (!existsSync41(dir)) return [];
19719
19980
  const suffix = "-sync.json";
19720
19981
  return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
@@ -19803,8 +20064,8 @@ function describeCause(err3) {
19803
20064
  }
19804
20065
 
19805
20066
  // src/daemon.ts
19806
- import { existsSync as existsSync42, mkdirSync as mkdirSync27, readFileSync as readFileSync37, readdirSync as readdirSync17, rmSync as rmSync18, statSync as statSync12, writeFileSync as writeFileSync29 } from "fs";
19807
- 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";
19808
20069
  import { randomBytes as randomBytes2 } from "crypto";
19809
20070
  var DEFAULT_FLOOR_MS = 5e3;
19810
20071
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -19892,7 +20153,7 @@ function latestMtime(path) {
19892
20153
  if (!stat2.isDirectory()) return 0;
19893
20154
  let latest = stat2.mtimeMs;
19894
20155
  for (const entry of readdirSync17(path, { withFileTypes: true })) {
19895
- latest = Math.max(latest, latestMtime(join43(path, entry.name)));
20156
+ latest = Math.max(latest, latestMtime(join44(path, entry.name)));
19896
20157
  }
19897
20158
  return latest;
19898
20159
  }
@@ -19904,7 +20165,7 @@ function makeStaleChecker(paths, opts = {}) {
19904
20165
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync42(opts.reloadSentinel);
19905
20166
  }
19906
20167
  function reloadSentinelFor(stateDir) {
19907
- return join43(stateDir, "reload-requested");
20168
+ return join44(stateDir, "reload-requested");
19908
20169
  }
19909
20170
  function readReloadRequestedAt(stateDir) {
19910
20171
  try {
@@ -19914,12 +20175,12 @@ function readReloadRequestedAt(stateDir) {
19914
20175
  }
19915
20176
  }
19916
20177
  function daemonStampFor(stateDir) {
19917
- return join43(stateDir, "daemon.started");
20178
+ return join44(stateDir, "daemon.started");
19918
20179
  }
19919
20180
  function noteDaemonStarted(stateDir, now = Date.now()) {
19920
20181
  mkdirSync27(stateDir, { recursive: true });
19921
- rmSync18(reloadSentinelFor(stateDir), { force: true });
19922
- 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 })}
19923
20184
  `);
19924
20185
  }
19925
20186
  function readDaemonStarted(stateDir) {
@@ -19989,7 +20250,7 @@ async function runOnePassUnguarded(o) {
19989
20250
  await hydrateApiKeys([o.route], o.getSessionStore);
19990
20251
  const emit = o.newEmitter();
19991
20252
  const decide = o.decide ?? decideCycle;
19992
- const spawn9 = o.spawnRoleAgent ?? runRoleAgent;
20253
+ const spawn10 = o.spawnRoleAgent ?? runRoleAgent;
19993
20254
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
19994
20255
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
19995
20256
  if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
@@ -20093,7 +20354,7 @@ async function runOnePassUnguarded(o) {
20093
20354
  settle = resolve7;
20094
20355
  });
20095
20356
  const ready = (tookTicket2) => settle(tookTicket2);
20096
- const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
20357
+ const run2 = spawn10(role, decision, ctx, { ready }).then(() => {
20097
20358
  breaker.recordSuccess(role);
20098
20359
  clearShipAttention(o.state, `role_parked:${role}`);
20099
20360
  clearShipAttention(o.state, workspaceAuthKey(role));
@@ -20488,17 +20749,17 @@ import {
20488
20749
  openSync as openSync5,
20489
20750
  readSync as readSync3,
20490
20751
  readdirSync as readdirSync18,
20491
- rmSync as rmSync19,
20752
+ rmSync as rmSync20,
20492
20753
  statSync as statSync13,
20493
20754
  watch
20494
20755
  } from "fs";
20495
20756
  import { createConnection } from "net";
20496
20757
  import { createServer } from "http";
20497
- import { basename as basename12, dirname as dirname23, join as join44 } from "path";
20498
- 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";
20499
20760
 
20500
20761
  // src/periscope-control.ts
20501
- import { spawn as spawn7 } from "child_process";
20762
+ import { spawn as spawn8 } from "child_process";
20502
20763
  var reply = (status2, body) => ({ status: status2, body });
20503
20764
  var err = (status2, message) => reply(status2, { error: message });
20504
20765
  var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
@@ -20689,7 +20950,7 @@ async function handlePeriscopes(h, q) {
20689
20950
  }
20690
20951
  function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20691
20952
  return new Promise((resolve7) => {
20692
- 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"] });
20693
20954
  let stdout = "";
20694
20955
  let stderr = "";
20695
20956
  let pending = "";
@@ -20723,7 +20984,7 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
20723
20984
  // src/control-api.ts
20724
20985
  var PAUSABLE = ["dev", "design", "qa"];
20725
20986
  var CONTROL_SOCKET_NAME = "control.sock";
20726
- var controlSocketFor = (stateDir) => join44(stateDir, CONTROL_SOCKET_NAME);
20987
+ var controlSocketFor = (stateDir) => join45(stateDir, CONTROL_SOCKET_NAME);
20727
20988
  var MAX_SOCKET_PATH = 100;
20728
20989
  var MAX_TAIL_BYTES = 1024 * 1024;
20729
20990
  var MAX_WAIT_SECONDS = 30;
@@ -20810,7 +21071,7 @@ function candidates(live, rotated) {
20810
21071
  const out = [live];
20811
21072
  if (!rotated) return out;
20812
21073
  try {
20813
- 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));
20814
21075
  } catch {
20815
21076
  }
20816
21077
  return out;
@@ -20933,7 +21194,7 @@ function pauseDoc(state) {
20933
21194
  }
20934
21195
  function describeStream(r) {
20935
21196
  return {
20936
- run: basename12(r.streamPath).replace(/\.jsonl$/, ""),
21197
+ run: basename13(r.streamPath).replace(/\.jsonl$/, ""),
20937
21198
  pid: r.pid,
20938
21199
  ticket: r.ticket,
20939
21200
  role: r.role,
@@ -21010,7 +21271,7 @@ async function route(deps, method, url, req, abort) {
21010
21271
  if (p.startsWith("/streams/") && get2) {
21011
21272
  const run2 = decodeURIComponent(p.slice("/streams/".length));
21012
21273
  if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
21013
- 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`);
21014
21275
  if (!existsSync43(file)) return err2(404, "no such run");
21015
21276
  return tailReply(file, url.searchParams, false, abort);
21016
21277
  }
@@ -21073,7 +21334,7 @@ async function startControlServer(deps) {
21073
21334
  log(`control API not started: socket path is too long for a unix socket (${path})`);
21074
21335
  return null;
21075
21336
  }
21076
- if (await socketIsStale(path)) rmSync19(path, { force: true });
21337
+ if (await socketIsStale(path)) rmSync20(path, { force: true });
21077
21338
  else if (existsSync43(path)) {
21078
21339
  log(`control API not started: ${path} is in use by another process`);
21079
21340
  return null;
@@ -21120,7 +21381,7 @@ async function startControlServer(deps) {
21120
21381
  path,
21121
21382
  close: () => new Promise((done) => {
21122
21383
  server.close(() => {
21123
- rmSync19(path, { force: true });
21384
+ rmSync20(path, { force: true });
21124
21385
  done();
21125
21386
  });
21126
21387
  })
@@ -21129,7 +21390,7 @@ async function startControlServer(deps) {
21129
21390
  });
21130
21391
  }
21131
21392
  var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
21132
- function runDetached(commands, spawnFn = spawn8) {
21393
+ function runDetached(commands, spawnFn = spawn9) {
21133
21394
  const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
21134
21395
  const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
21135
21396
  child.on("error", () => {
@@ -21147,7 +21408,7 @@ function schedulerHandler(control, run2 = runDetached) {
21147
21408
  }
21148
21409
  function crewJson(args, timeoutMs = 6e4) {
21149
21410
  return new Promise((resolve7) => {
21150
- 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) => {
21151
21412
  try {
21152
21413
  const v = JSON.parse(String(stdout));
21153
21414
  resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
@@ -21160,24 +21421,24 @@ function crewJson(args, timeoutMs = 6e4) {
21160
21421
 
21161
21422
  // src/periscope-mcp.ts
21162
21423
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
21163
- 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";
21164
21425
  import { z } from "zod";
21165
21426
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21166
21427
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
21167
21428
 
21168
21429
  // src/periscope-endpoints.ts
21169
21430
  import { randomBytes as randomBytes3 } from "crypto";
21170
- import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync28, readdirSync as readdirSync19, readFileSync as readFileSync38, rmSync as rmSync20, writeFileSync as writeFileSync30 } from "fs";
21171
- import { join as join45 } from "path";
21172
- var secretDir = (stateDir) => join45(stateDir, "periscope-mcp");
21173
- var secretFile = (stateDir, id) => join45(secretDir(stateDir), `${id}.json`);
21174
- var endpointDir = (stateDir) => join45(stateDir, "periscope-endpoints");
21175
- 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`);
21176
21437
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
21177
21438
  mkdirSync28(secretDir(stateDir), { recursive: true, mode: 448 });
21178
21439
  const secret = randomBytes3(32).toString("base64url");
21179
21440
  const file = secretFile(stateDir, ticketRecordId);
21180
- writeFileSync30(file, `${JSON.stringify({ secret })}
21441
+ writeFileSync31(file, `${JSON.stringify({ secret })}
21181
21442
  `, { mode: 384 });
21182
21443
  chmodSync5(file, 384);
21183
21444
  return secret;
@@ -21191,7 +21452,7 @@ function readPeriscopeSecret(stateDir, ticketRecordId) {
21191
21452
  }
21192
21453
  }
21193
21454
  function removePeriscopeSecret(stateDir, ticketRecordId) {
21194
- rmSync20(secretFile(stateDir, ticketRecordId), { force: true });
21455
+ rmSync21(secretFile(stateDir, ticketRecordId), { force: true });
21195
21456
  }
21196
21457
  function readPeriscopeEndpoint(stateDir, ticketRecordId) {
21197
21458
  try {
@@ -21202,11 +21463,11 @@ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
21202
21463
  }
21203
21464
  function writePeriscopeEndpoint(stateDir, rec) {
21204
21465
  mkdirSync28(endpointDir(stateDir), { recursive: true });
21205
- writeFileSync30(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
21466
+ writeFileSync31(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
21206
21467
  `);
21207
21468
  }
21208
21469
  function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
21209
- rmSync20(endpointFile(stateDir, ticketRecordId), { force: true });
21470
+ rmSync21(endpointFile(stateDir, ticketRecordId), { force: true });
21210
21471
  }
21211
21472
  function listPeriscopeEndpoints(stateDir) {
21212
21473
  const dir = endpointDir(stateDir);
@@ -21232,7 +21493,8 @@ async function registerPeriscopeEndpoint(d, p) {
21232
21493
  const out = await registerEndpoint(api, p.workspaceId, stored?.endpointId, {
21233
21494
  label: `Periscope: ${d.shipName} ${p.ticket}`,
21234
21495
  url,
21235
- mcpSecret: secret
21496
+ mcpSecret: secret,
21497
+ discoverable: false
21236
21498
  });
21237
21499
  if (out.kind !== "ok") {
21238
21500
  return { ok: false, message: out.kind === "forbidden" ? "registering the endpoint was refused (403): the route key lacks REGISTER_MCP_ENDPOINTS" : out.message };
@@ -21302,7 +21564,7 @@ async function realAncestor(p) {
21302
21564
  }
21303
21565
  const parent = dirname24(cur);
21304
21566
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
21305
- tail3.push(basename13(cur));
21567
+ tail3.push(basename14(cur));
21306
21568
  cur = parent;
21307
21569
  }
21308
21570
  }
@@ -21312,13 +21574,13 @@ async function resolveInWorktree(root, requested) {
21312
21574
  let abs;
21313
21575
  if (isAbsolute3(requested)) {
21314
21576
  abs = resolve5(requested);
21315
- if (isInside(root, abs)) abs = join46(realRoot, relative2(root, abs));
21577
+ if (isInside(root, abs)) abs = join47(realRoot, relative2(root, abs));
21316
21578
  } else {
21317
21579
  abs = resolve5(realRoot, requested);
21318
21580
  }
21319
21581
  if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21320
21582
  const { real, rest } = await realAncestor(abs);
21321
- const final = rest ? join46(real, rest) : real;
21583
+ const final = rest ? join47(real, rest) : real;
21322
21584
  if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
21323
21585
  if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
21324
21586
  return final;
@@ -21386,7 +21648,7 @@ function createPeriscopeMcpServer(d, sc) {
21386
21648
  const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
21387
21649
  const lines2 = await Promise.all(entries.map(async (e) => {
21388
21650
  if (e.isDirectory()) return `[DIR] ${e.name}`;
21389
- const size = (await lstat(join46(abs, e.name))).size;
21651
+ const size = (await lstat(join47(abs, e.name))).size;
21390
21652
  return `[FILE] ${e.name} (${size} bytes)`;
21391
21653
  }));
21392
21654
  return text(lines2.join("\n") || "(empty)");
@@ -21452,8 +21714,8 @@ function createPeriscopeMcpServer(d, sc) {
21452
21714
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
21453
21715
  }, async ({ path }) => {
21454
21716
  const parent = await inTree(dirname24(path) === "" ? "." : dirname24(path));
21455
- const abs = join46(parent, basename13(path));
21456
- 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);
21457
21719
  const st = await lstat(abs).catch(() => void 0);
21458
21720
  if (!st) return text(`${path} does not exist`, true);
21459
21721
  if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
@@ -22047,6 +22309,7 @@ if (command === "passenger-sync-daemon") {
22047
22309
  shipName: cfg.ship.name,
22048
22310
  apiFor: endpointApiFor(routes),
22049
22311
  urlFor: (slug) => publicUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
22312
+ controlUrlFor: (slug) => `${relayBaseUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control`,
22050
22313
  warn: (m) => emit.warn(m, { step: "passengers" })
22051
22314
  };
22052
22315
  }, releaseRefused = function() {
@@ -22151,7 +22414,7 @@ if (command === "passenger-sync-daemon") {
22151
22414
  stateDir: cfg.ship.stateDir,
22152
22415
  configFile: cfg.configFile,
22153
22416
  readFile: (p) => readFileSync39(p, "utf8"),
22154
- writeFile: (p, t) => writeFileSync31(p, t),
22417
+ writeFile: (p, t) => writeFileSync32(p, t),
22155
22418
  stdin: () => readFileSync39(0, "utf8"),
22156
22419
  env: process.env
22157
22420
  }
@@ -22435,7 +22698,7 @@ if (command === "passenger-sync-daemon") {
22435
22698
  }
22436
22699
  if (host === "linux" && hasSystemd) {
22437
22700
  const label = labelFor(CREW_HOME, "run");
22438
- const staleTimer = join47(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22701
+ const staleTimer = join48(homedir11(), ".config", "systemd", "user", `${label}.timer`);
22439
22702
  if (existsSync45(staleTimer)) {
22440
22703
  process.stdout.write(
22441
22704
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -22498,7 +22761,7 @@ if (command === "passenger-sync-daemon") {
22498
22761
  for (const a of plan.actions) {
22499
22762
  if (a.kind === "enable") {
22500
22763
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
22501
- writeFileSync31(cfg.configFile, planEnable(readFileSync39(cfg.configFile, "utf8"), a.route));
22764
+ writeFileSync32(cfg.configFile, planEnable(readFileSync39(cfg.configFile, "utf8"), a.route));
22502
22765
  enabledSound.add(a.route);
22503
22766
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
22504
22767
  } else {
@@ -23621,7 +23884,7 @@ ${cu.tail.trimEnd()}
23621
23884
  process.exit(1);
23622
23885
  }
23623
23886
  requireArmed("run the daemon loop");
23624
- 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")];
23625
23888
  const configPaths = daemonConfigPaths(cfg);
23626
23889
  watchPaths.push(...configPaths);
23627
23890
  noteDaemonStarted(cfg.ship.stateDir);
@@ -23920,7 +24183,7 @@ ${cu.tail.trimEnd()}
23920
24183
  const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync39(resolvedPath, "utf8")) : {};
23921
24184
  raw.agentSkills = result.agentSkills;
23922
24185
  mkdirSync29(dirOf2(resolvedPath), { recursive: true });
23923
- writeFileSync31(resolvedPath, `${JSON.stringify(raw, null, 2)}
24186
+ writeFileSync32(resolvedPath, `${JSON.stringify(raw, null, 2)}
23924
24187
  `);
23925
24188
  }
23926
24189
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -23943,7 +24206,7 @@ ${cu.tail.trimEnd()}
23943
24206
  throw e;
23944
24207
  }
23945
24208
  const oldName = cfg.ship.name;
23946
- writeFileSync31(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), newName));
24209
+ writeFileSync32(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), newName));
23947
24210
  cfg.ship.name = newName;
23948
24211
  let renameMachineId;
23949
24212
  try {
@@ -24072,19 +24335,19 @@ ${cu.tail.trimEnd()}
24072
24335
  let cleared;
24073
24336
  if (clear2) {
24074
24337
  const plan = clearRouteHooks(configText, a.route, a.name);
24075
- if (plan.removed.length > 0) writeFileSync31(cfg.configFile, plan.text);
24338
+ if (plan.removed.length > 0) writeFileSync32(cfg.configFile, plan.text);
24076
24339
  cleared = { file: cfg.configFile, removed: plan.removed };
24077
24340
  }
24078
24341
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
24079
24342
  if (write === "route") {
24080
24343
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
24081
- writeFileSync31(cfg.configFile, plan.text);
24344
+ writeFileSync32(cfg.configFile, plan.text);
24082
24345
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
24083
24346
  } else if (write === "repo") {
24084
24347
  const have = existingRepoConfig(files);
24085
24348
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
24086
24349
  const file = resolvePath(dir, ".crew.yaml");
24087
- writeFileSync31(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24350
+ writeFileSync32(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
24088
24351
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
24089
24352
  }
24090
24353
  if (flag("json")) {
@@ -24251,7 +24514,7 @@ ${cu.tail.trimEnd()}
24251
24514
  throw new ReposError(`${e.message}${authHint}
24252
24515
  crew.yaml was not changed`);
24253
24516
  }
24254
- writeFileSync31(cfg.configFile, plan2.text);
24517
+ writeFileSync32(cfg.configFile, plan2.text);
24255
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:
24256
24519
  `);
24257
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}`);
@@ -24314,11 +24577,11 @@ ${cu.tail.trimEnd()}
24314
24577
  const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
24315
24578
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
24316
24579
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
24317
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
24580
+ writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
24318
24581
  `);
24319
24582
  }
24320
24583
  }
24321
- writeFileSync31(cfg.configFile, plan.text);
24584
+ writeFileSync32(cfg.configFile, plan.text);
24322
24585
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
24323
24586
  `);
24324
24587
  } catch (e) {
@@ -24687,7 +24950,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24687
24950
  process.exit(2);
24688
24951
  }
24689
24952
  if (found.shipNameAdopted && !dryRun) {
24690
- writeFileSync31(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), found.shipNameAdopted));
24953
+ writeFileSync32(cfg.configFile, setShipNameInYaml(readFileSync39(cfg.configFile, "utf8"), found.shipNameAdopted));
24691
24954
  cfg.ship.name = found.shipNameAdopted;
24692
24955
  }
24693
24956
  if (found.shipRow) {
@@ -24866,7 +25129,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24866
25129
  mkdirSync29(dirname25(resolvedPath), { recursive: true });
24867
25130
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
24868
25131
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
24869
- writeFileSync31(tmpPath, `${JSON.stringify(resolved, null, 2)}
25132
+ writeFileSync32(tmpPath, `${JSON.stringify(resolved, null, 2)}
24870
25133
  `);
24871
25134
  renameSync5(tmpPath, resolvedPath);
24872
25135
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -24913,7 +25176,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
24913
25176
  `);
24914
25177
  } else {
24915
25178
  mkdirSync29(dirname25(apiKeyFile), { recursive: true });
24916
- writeFileSync31(apiKeyFile, `${apiKeyVar}=${apiKey}
25179
+ writeFileSync32(apiKeyFile, `${apiKeyVar}=${apiKey}
24917
25180
  `, { mode: 384 });
24918
25181
  process.stderr.write(`API key written to ${apiKeyFile}
24919
25182
  `);
@@ -25743,10 +26006,10 @@ ${where.join("\n")}`);
25743
26006
  },
25744
26007
  prepare: async (path) => {
25745
26008
  for (const f of repoP.config.worktrees.copy) {
25746
- const src = join47(repoP.dir, f);
26009
+ const src = join48(repoP.dir, f);
25747
26010
  if (!existsSync45(src)) continue;
25748
- mkdirSync29(dirOf2(join47(path, f)), { recursive: true });
25749
- copyFileSync2(src, join47(path, f));
26011
+ mkdirSync29(dirOf2(join48(path, f)), { recursive: true });
26012
+ copyFileSync2(src, join48(path, f));
25750
26013
  }
25751
26014
  const setup = repoP.config.hooks.setup;
25752
26015
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -26095,7 +26358,7 @@ ${where.join("\n")}`);
26095
26358
  break;
26096
26359
  }
26097
26360
  mkdirSync29(cfg.ship.stateDir, { recursive: true });
26098
- writeFileSync31(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26361
+ writeFileSync32(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
26099
26362
  `);
26100
26363
  process.stdout.write(
26101
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"
@@ -26200,7 +26463,7 @@ ${where.join("\n")}`);
26200
26463
  }
26201
26464
  if (configured2.some((r) => r.dir === dir)) {
26202
26465
  try {
26203
- writeFileSync31(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26466
+ writeFileSync32(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
26204
26467
  } catch (e) {
26205
26468
  if (e instanceof ReposError) fail(e.message);
26206
26469
  throw e;
@@ -26212,16 +26475,16 @@ ${where.join("\n")}`);
26212
26475
  const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26213
26476
  if (raw.repoNames) delete raw.repoNames[row.id];
26214
26477
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
26215
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
26478
+ writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
26216
26479
  `);
26217
26480
  }
26218
26481
  }
26219
- 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 });
26220
26483
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
26221
26484
  break;
26222
26485
  }
26223
26486
  const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
26224
- const fresh = !existsSync45(join47(dir, ".git"));
26487
+ const fresh = !existsSync45(join48(dir, ".git"));
26225
26488
  if (existsSync45(dir) && fresh && readdirSync20(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
26226
26489
  if (dryRun) {
26227
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`);
@@ -26229,8 +26492,8 @@ ${where.join("\n")}`);
26229
26492
  }
26230
26493
  if (fresh) {
26231
26494
  mkdirSync29(dir, { recursive: true });
26232
- writeFileSync31(join47(dir, "README.md"), SAMPLE_README);
26233
- 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);
26234
26497
  const g = (...a) => {
26235
26498
  const r = spawnSync10("git", a, { cwd: dir, encoding: "utf8" });
26236
26499
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -26252,13 +26515,13 @@ ${where.join("\n")}`);
26252
26515
  const raw = JSON.parse(readFileSync39(resolvedFile, "utf8"));
26253
26516
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
26254
26517
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
26255
- writeFileSync31(resolvedFile, `${JSON.stringify(raw, null, 2)}
26518
+ writeFileSync32(resolvedFile, `${JSON.stringify(raw, null, 2)}
26256
26519
  `);
26257
26520
  }
26258
26521
  }
26259
26522
  if (!configured2.some((r) => r.dir === dir)) {
26260
26523
  try {
26261
- 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);
26262
26525
  } catch (e) {
26263
26526
  if (e instanceof ReposError) fail(e.message);
26264
26527
  throw e;
@@ -26493,7 +26756,7 @@ ${where.join("\n")}`);
26493
26756
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
26494
26757
  // config-provenance display; this is the field that actually
26495
26758
  // describes where a worktree lands by default.
26496
- 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>`),
26497
26760
  worktreeParent: resolvePath(r.dir, "..")
26498
26761
  })),
26499
26762
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -26751,7 +27014,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
26751
27014
  );
26752
27015
  for (const r of await resolvedRepos(route2)) {
26753
27016
  const { config } = r;
26754
- 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)}`);
26755
27018
  process.stdout.write(
26756
27019
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
26757
27020
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -27073,6 +27336,13 @@ ${foreign.join("\n")}
27073
27336
  `);
27074
27337
  }
27075
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
+ }
27076
27346
  const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
27077
27347
  if (passengerRoutes.length === 0) {
27078
27348
  process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
@@ -27122,6 +27392,9 @@ ${foreign.join("\n")}
27122
27392
  ` ${r.route} url: ${slug ? publicUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain) : "not minted yet (first tunnel cycle creates it)"}
27123
27393
  `
27124
27394
  );
27395
+ const controlUrl = slug && readPersistedTunnel(cfg.ship.stateDir, ws)?.controlSocket ? `${relayBaseUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control` : void 0;
27396
+ process.stdout.write(` ${r.route} control url: ${controlUrl ?? "none (the tunnel carries passengers only)"}
27397
+ `);
27125
27398
  const api = endpointApiFor([r])(ws);
27126
27399
  if (!api) continue;
27127
27400
  const probe = await probeEndpointAccess(api, ws);
@@ -27140,7 +27413,7 @@ ${foreign.join("\n")}
27140
27413
  }
27141
27414
  for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
27142
27415
  const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
27143
- process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret})
27416
+ process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret}, attached only)
27144
27417
  `);
27145
27418
  }
27146
27419
  if (cfg.ship.relayHost && !flag("offline")) {