@tablation/crew 0.108.0 → 0.110.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 +500 -154
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -243,7 +243,7 @@ function shellInitScript(shell) {
243
243
 
244
244
  // src/config.ts
245
245
  import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
246
- import { resolve, dirname as dirname4, isAbsolute, join as join12, basename as basename3 } from "path";
246
+ import { resolve, dirname as dirname4, isAbsolute, join as join12, basename as basename4 } from "path";
247
247
  import { homedir as homedir6, tmpdir } from "os";
248
248
  import { parse as parse2 } from "yaml";
249
249
 
@@ -2071,8 +2071,204 @@ function explain(ship, required) {
2071
2071
 
2072
2072
  // src/repo-config.ts
2073
2073
  import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
2074
- import { join as join11, basename as basename2 } from "path";
2074
+ import { join as join11, basename as basename3 } from "path";
2075
2075
  import { parse } from "yaml";
2076
+
2077
+ // src/sandbox-policy.ts
2078
+ import { basename as basename2 } from "path";
2079
+ import { stringify } from "yaml";
2080
+ var SandboxPolicyError = class extends Error {
2081
+ };
2082
+ var PROVIDER_HOSTS = {
2083
+ anthropic: ["api.anthropic.com", "claude.ai", "platform.claude.com"],
2084
+ openai: ["api.openai.com", "auth.openai.com", "chatgpt.com"],
2085
+ google: ["generativelanguage.googleapis.com", "oauth2.googleapis.com", "cloudcode-pa.googleapis.com"]
2086
+ };
2087
+ var TOOLCHAIN_HOSTS = {
2088
+ corepack: ["registry.npmjs.org"],
2089
+ pnpm: ["registry.npmjs.org"],
2090
+ yarn: ["registry.npmjs.org"],
2091
+ npm: ["registry.npmjs.org"],
2092
+ prisma: ["binaries.prisma.sh"],
2093
+ playwright: ["cdn.playwright.dev", "playwright.download.prss.microsoft.com"],
2094
+ pip: ["pypi.org", "files.pythonhosted.org"],
2095
+ cargo: ["index.crates.io", "static.crates.io"],
2096
+ go: ["proxy.golang.org", "sum.golang.org"],
2097
+ swift: ["download.swift.org"]
2098
+ };
2099
+ var FORBIDDEN_HOSTS = /* @__PURE__ */ new Set(["169.254.169.254", "fd00:ec2::254", "metadata.google.internal", "metadata", "localhost"]);
2100
+ var IMDS_ADDRESS = "169.254.169.254";
2101
+ var HOST = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/;
2102
+ var IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
2103
+ function isForbiddenIp(host) {
2104
+ const m = IPV4.exec(host);
2105
+ if (m) {
2106
+ const [a, b] = [Number(m[1]), Number(m[2])];
2107
+ return a === 127 || a === 0 || a === 10 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
2108
+ }
2109
+ return host.includes(":");
2110
+ }
2111
+ function isAmbiguousNumericHost(host) {
2112
+ const labels = host.split(".");
2113
+ if (!labels.every((l) => /^(0x[0-9a-f]*|\d+)$/.test(l))) return false;
2114
+ const canonical = labels.length === 4 && labels.every((l) => /^(0|[1-9]\d{0,2})$/.test(l) && Number(l) <= 255);
2115
+ return !canonical;
2116
+ }
2117
+ function parseEndpoint(raw, defaultPort = 443) {
2118
+ const s = raw.trim().toLowerCase();
2119
+ const m = /^([^:/\s]+)(?::(\d{1,5}))?$/.exec(s);
2120
+ if (!m) throw new SandboxPolicyError(`"${raw}" is not a host or host:port`);
2121
+ const host = m[1];
2122
+ const port = m[2] ? Number(m[2]) : defaultPort;
2123
+ if (port < 1 || port > 65535) throw new SandboxPolicyError(`"${raw}" has an invalid port`);
2124
+ if (FORBIDDEN_HOSTS.has(host) || isForbiddenIp(host)) {
2125
+ throw new SandboxPolicyError(`"${raw}" is never reachable from a sandbox (loopback, private, link-local and cloud metadata addresses are always denied)`);
2126
+ }
2127
+ if (isAmbiguousNumericHost(host)) {
2128
+ throw new SandboxPolicyError(`"${raw}" is a numeric host in a non-canonical form (hex, octal, integer or short dotted); write an IPv4 address as four plain decimal octets or use a DNS name`);
2129
+ }
2130
+ if (!IPV4.test(host) && !HOST.test(host)) throw new SandboxPolicyError(`"${raw}" is not a valid host name`);
2131
+ return { host, port };
2132
+ }
2133
+ function dedupe(eps) {
2134
+ const seen = /* @__PURE__ */ new Set();
2135
+ return eps.filter((e) => {
2136
+ const k = `${e.host}:${e.port}`;
2137
+ if (seen.has(k)) return false;
2138
+ seen.add(k);
2139
+ return true;
2140
+ });
2141
+ }
2142
+ function tablationEndpoint(url) {
2143
+ let u;
2144
+ try {
2145
+ u = new URL(url);
2146
+ } catch {
2147
+ throw new SandboxPolicyError(`the route's base URL "${url}" is not a URL`);
2148
+ }
2149
+ const port = u.port ? Number(u.port) : u.protocol === "http:" ? 80 : 443;
2150
+ const host = u.hostname.toLowerCase();
2151
+ if (host !== "localhost" && (FORBIDDEN_HOSTS.has(host) || host.startsWith("169.254."))) {
2152
+ throw new SandboxPolicyError(`the route's base URL host "${host}" is never reachable from a sandbox`);
2153
+ }
2154
+ return { host, port };
2155
+ }
2156
+ var TOOL_BINARIES = ["/usr/bin/curl", "/usr/bin/node", "/usr/local/bin/node", "/usr/bin/npm", "/usr/local/bin/npm", "/usr/local/bin/pnpm", "/usr/bin/corepack", "/usr/local/bin/corepack"];
2157
+ var GIT_BINARIES = ["/usr/bin/git", "/usr/lib/git-core/git-remote-http", "/usr/lib/git-core/git-remote-https"];
2158
+ function policyGroups(input) {
2159
+ const harness = `/usr/local/bin/${basename2(input.harnessBin)}`;
2160
+ const harnessBins = [.../* @__PURE__ */ new Set([harness, `/usr/bin/${basename2(input.harnessBin)}`, "/usr/bin/node", "/usr/local/bin/node"])];
2161
+ const groups = [];
2162
+ const tablation = tablationEndpoint(input.tablationUrl);
2163
+ groups.push({
2164
+ name: "tablation",
2165
+ purpose: input.authMode === "workspace" ? "the tracker and the inference endpoint (the only route to a model)" : "the tracker",
2166
+ endpoints: [tablation],
2167
+ // curl too: the lane briefs drive the tracker API with it.
2168
+ binaries: [...harnessBins, "/usr/bin/curl"]
2169
+ });
2170
+ const forge = dedupe((input.forgeHosts.length > 0 ? input.forgeHosts : ["github.com"]).map((h) => parseEndpoint(h)));
2171
+ groups.push({ name: "forge", purpose: "clone and push the ticket branch", endpoints: forge, binaries: GIT_BINARIES });
2172
+ if (input.authMode !== "workspace") {
2173
+ const hosts = PROVIDER_HOSTS[input.provider];
2174
+ if (hosts) {
2175
+ groups.push({ name: "model", purpose: `the ${input.provider} model route (auth: ${input.authMode})`, endpoints: hosts.map((h) => parseEndpoint(h)), binaries: harnessBins });
2176
+ }
2177
+ }
2178
+ const toolHosts = (input.toolchains ?? []).flatMap((t) => {
2179
+ const hosts = TOOLCHAIN_HOSTS[t.toLowerCase()];
2180
+ if (!hosts) throw new SandboxPolicyError(`sandbox toolchain "${t}" is not one crew knows the download hosts of (known: ${Object.keys(TOOLCHAIN_HOSTS).join(", ")}); declare its hosts under sandbox.hosts instead`);
2181
+ return hosts.map((h) => parseEndpoint(h));
2182
+ });
2183
+ if (toolHosts.length > 0) {
2184
+ groups.push({ name: "toolchains", purpose: "run-time downloads of the declared toolchains", endpoints: dedupe(toolHosts), binaries: TOOL_BINARIES });
2185
+ }
2186
+ const extras = dedupe((input.extraHosts ?? []).map((h) => parseEndpoint(h)));
2187
+ if (extras.length > 0) {
2188
+ groups.push({ name: "declared", purpose: "hosts the repository's sandbox: section declares", endpoints: extras, binaries: [.../* @__PURE__ */ new Set([...TOOL_BINARIES, ...harnessBins])] });
2189
+ }
2190
+ return groups;
2191
+ }
2192
+ function generateSandboxPolicy(input) {
2193
+ const groups = policyGroups(input);
2194
+ const mirror = input.mirrorDir ?? "/cache";
2195
+ const work = input.workDir ?? "/sandbox/work";
2196
+ const doc = {
2197
+ version: 1,
2198
+ // Fixed to the image's defaults; only the cache mount is added, read-only.
2199
+ filesystem_policy: {
2200
+ include_workdir: true,
2201
+ read_only: ["/usr", "/lib", "/proc", "/dev/urandom", "/app", "/etc", "/var/log", "/opt/crew", mirror],
2202
+ read_write: ["/sandbox", "/tmp", "/dev/null", work]
2203
+ },
2204
+ // A host that cannot enforce fails the run instead of running unconfined.
2205
+ landlock: { compatibility: "hard_requirement" },
2206
+ network_policies: Object.fromEntries(groups.map((g) => [g.name, {
2207
+ name: g.name,
2208
+ endpoints: g.endpoints.map((e) => ({ host: e.host, port: e.port })),
2209
+ binaries: g.binaries.map((path) => ({ path }))
2210
+ }]))
2211
+ };
2212
+ const header = [
2213
+ "# Generated by crew for one sandboxed run. Do not edit: the next run replaces it.",
2214
+ `# Auth mode: ${input.authMode}. Everything not listed below is denied, including package registries.`,
2215
+ `# The cloud metadata address ${IMDS_ADDRESS} is denied; no rule here can match it.`,
2216
+ ...groups.map((g) => `# ${g.name}: ${g.purpose}`),
2217
+ ""
2218
+ ].join("\n");
2219
+ return { yaml: header + stringify(doc), groups, extraHosts: (input.extraHosts ?? []).map((h) => h.trim().toLowerCase()) };
2220
+ }
2221
+ function policyPathFor(streamPath) {
2222
+ return streamPath.replace(/\.jsonl$/, "") + ".policy.yaml";
2223
+ }
2224
+ function parseSandboxNetwork(raw, file, fail) {
2225
+ if (raw === void 0 || raw === null) return void 0;
2226
+ if (typeof raw !== "object" || Array.isArray(raw)) throw fail(`${file}: sandbox must be a mapping`);
2227
+ const r = raw;
2228
+ const list = (v, where) => {
2229
+ if (v === void 0 || v === null) return [];
2230
+ if (!Array.isArray(v) || v.some((x) => typeof x !== "string" || x.trim() === "")) throw fail(`${file}: ${where} must be a list of non-empty strings`);
2231
+ return v;
2232
+ };
2233
+ const hosts = list(r.hosts, "sandbox.hosts");
2234
+ const toolchains = list(r.toolchains, "sandbox.toolchains");
2235
+ for (const h of hosts) {
2236
+ try {
2237
+ parseEndpoint(h);
2238
+ } catch (e) {
2239
+ throw fail(`${file}: sandbox.hosts entry ${e.message}`);
2240
+ }
2241
+ }
2242
+ for (const t of toolchains) {
2243
+ if (!TOOLCHAIN_HOSTS[t.toLowerCase()]) throw fail(`${file}: sandbox.toolchains entry "${t}" is not known (known: ${Object.keys(TOOLCHAIN_HOSTS).join(", ")})`);
2244
+ }
2245
+ if (hosts.length === 0 && toolchains.length === 0) return void 0;
2246
+ return { hosts, toolchains };
2247
+ }
2248
+ function parseDeniedRequests(log) {
2249
+ const out = /* @__PURE__ */ new Map();
2250
+ for (const line of log.split("\n")) {
2251
+ if (!/\bden(y|ied|ies|ying)\b/i.test(line)) continue;
2252
+ const pick = (keys) => {
2253
+ for (const k of keys) {
2254
+ const m = new RegExp(`\\b${k}[=:]\\s*"?([^\\s",}]+)`, "i").exec(line);
2255
+ if (m) return m[1];
2256
+ }
2257
+ return void 0;
2258
+ };
2259
+ const host = pick(["dst_host", "destination_host", "host"]) ?? "?";
2260
+ const portRaw = pick(["dst_port", "destination_port", "port"]);
2261
+ const binary = pick(["binary", "bin", "exe", "process"]);
2262
+ const port = portRaw && /^\d+$/.test(portRaw) ? Number(portRaw) : void 0;
2263
+ const key = `${host}|${port ?? ""}|${binary ?? ""}`;
2264
+ const prior = out.get(key);
2265
+ if (prior) prior.count++;
2266
+ else out.set(key, { host, ...port !== void 0 ? { port } : {}, ...binary ? { binary } : {}, count: 1, line: line.trim().slice(0, 400) });
2267
+ }
2268
+ return [...out.values()];
2269
+ }
2270
+
2271
+ // src/repo-config.ts
2076
2272
  var SPEC_VERSION = 1;
2077
2273
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
2078
2274
  var RepoConfigError = class extends Error {
@@ -2204,6 +2400,7 @@ function parseSandbox(raw, file) {
2204
2400
  throw new RepoConfigError(`${file}: sandbox.image and sandbox.packages are alternatives \u2014 a published image already carries its packages; put the rest in sandbox.setup`);
2205
2401
  }
2206
2402
  }
2403
+ if (!image && packages.length === 0 && setup.length === 0 && warm.length === 0) return void 0;
2207
2404
  return { ...image ? { image } : {}, packages, setup, warm };
2208
2405
  }
2209
2406
  function parseRepoConfig(text2, file) {
@@ -2224,7 +2421,8 @@ function parseRepoConfig(text2, file) {
2224
2421
  rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
2225
2422
  rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
2226
2423
  rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
2227
- rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup", "warm"]), "sandbox", file);
2424
+ rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup", "warm", "hosts", "toolchains"]), "sandbox", file);
2425
+ const sandboxNetwork = parseSandboxNetwork(raw.sandbox, file, (m) => new RepoConfigError(m));
2228
2426
  if (raw.version === void 0) throw new RepoConfigError(`${file}: version is required`);
2229
2427
  if (raw.version !== SPEC_VERSION) {
2230
2428
  throw new RepoConfigError(
@@ -2415,6 +2613,7 @@ function parseRepoConfig(text2, file) {
2415
2613
  shell: raw.shell ? String(raw.shell) : void 0,
2416
2614
  hooks,
2417
2615
  labels,
2616
+ ...sandboxNetwork ? { sandboxNetwork } : {},
2418
2617
  release: {
2419
2618
  mode,
2420
2619
  ship,
@@ -2475,7 +2674,7 @@ function renderWorktreeName(template, ctx) {
2475
2674
  return renderBranchName(template.replace(/\{dir\}/g, ctx.dir), ctx);
2476
2675
  }
2477
2676
  function defaultWorktreePrefix(dir) {
2478
- const name = basename2(dir.replace(/[/\\]+$/, ""));
2677
+ const name = basename3(dir.replace(/[/\\]+$/, ""));
2479
2678
  return name ? `${name}-issue-` : "issue-";
2480
2679
  }
2481
2680
  var DEFAULTS = {
@@ -2660,7 +2859,7 @@ function effectiveBranchTemplate(cfg, prefix) {
2660
2859
  function effectiveWorktreeDirName(cfg, dir, branchName, number, renderName) {
2661
2860
  if (cfg.worktrees.name && renderName) return renderName(cfg.worktrees.name);
2662
2861
  if (cfg.provenance["worktrees.prefix"] !== "default") return `${cfg.worktrees.prefix}${number}`;
2663
- return `${basename2(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
2862
+ return `${basename3(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
2664
2863
  }
2665
2864
  function validateEffective(cfg) {
2666
2865
  const problems = [];
@@ -3483,7 +3682,7 @@ function dirForRepo(r, repoId) {
3483
3682
  }
3484
3683
  function repoTargetFor(r, repoId) {
3485
3684
  const names = r.resolved?.repoNames;
3486
- if (!repoId || !names) return Object.keys(r.repos).length || r.dirIsCloneBase ? null : { name: basename3(r.dir), dir: r.dir };
3685
+ if (!repoId || !names) return Object.keys(r.repos).length || r.dirIsCloneBase ? null : { name: basename4(r.dir), dir: r.dir };
3487
3686
  const name = names[repoId];
3488
3687
  if (!name) return null;
3489
3688
  return reposOf(r).find((t) => t.name === name) ?? null;
@@ -3585,7 +3784,7 @@ function reposOf(r) {
3585
3784
  ...hosts[id] ? { forgeHost: hosts[id] } : {}
3586
3785
  }));
3587
3786
  }
3588
- if (trackerNames.length <= 1) return [{ name: basename3(r.dir), dir: r.dir }];
3787
+ if (trackerNames.length <= 1) return [{ name: basename4(r.dir), dir: r.dir }];
3589
3788
  const remotes = r.resolved?.repoRemotes ?? {};
3590
3789
  const remoteByName = {};
3591
3790
  const hostByName = {};
@@ -4490,7 +4689,7 @@ import {
4490
4689
  readdirSync as readdirSync4,
4491
4690
  rmSync as rmSync8
4492
4691
  } from "fs";
4493
- import { dirname as dirname6, basename as basename4, join as join14 } from "path";
4692
+ import { dirname as dirname6, basename as basename5, join as join14 } from "path";
4494
4693
  var GitError = class extends Error {
4495
4694
  };
4496
4695
  function streamText(x) {
@@ -4634,7 +4833,7 @@ function deleteRemoteBranch(cwd, remote, branch) {
4634
4833
  }
4635
4834
  function worktreeForNumber(cwd, num) {
4636
4835
  const re = new RegExp(`(?:^|-)${num}$`);
4637
- return worktrees(cwd).find((w) => re.test(basename4(w.path))) ?? null;
4836
+ return worktrees(cwd).find((w) => re.test(basename5(w.path))) ?? null;
4638
4837
  }
4639
4838
  var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
4640
4839
  var resolve2 = (cwd, ref) => gitOk(cwd, ["rev-parse", "-q", "--verify", `${ref}^{commit}`]);
@@ -4871,7 +5070,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
4871
5070
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
4872
5071
  gitOk(cwd, ["worktree", "remove", "--force", path]);
4873
5072
  const root = dirname6(path);
4874
- if (basename4(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
5073
+ if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
4875
5074
  }
4876
5075
  }
4877
5076
  const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
@@ -5377,8 +5576,8 @@ function attentionProblems(items, already) {
5377
5576
  import { createHash as createHash6 } from "crypto";
5378
5577
  import { spawn as spawn4 } from "child_process";
5379
5578
  import { execFileSync as execFileSync6 } from "child_process";
5380
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync21, renameSync as renameSync3, writeFileSync as writeFileSync15, rmSync as rmSync13 } from "fs";
5381
- import { dirname as dirname9, join as join26 } from "path";
5579
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, renameSync as renameSync3, writeFileSync as writeFileSync15, rmSync as rmSync13 } from "fs";
5580
+ import { dirname as dirname10, join as join26 } from "path";
5382
5581
  import { tmpdir as tmpdir3 } from "os";
5383
5582
 
5384
5583
  // src/passenger-containers.ts
@@ -5399,10 +5598,10 @@ import { join as join24 } from "path";
5399
5598
 
5400
5599
  // src/agent.ts
5401
5600
  import { spawnSync as spawnSync5 } from "child_process";
5402
- 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";
5601
+ import { readFileSync as readFileSync19, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
5403
5602
  import { StringDecoder } from "string_decoder";
5404
5603
  import { finished } from "stream/promises";
5405
- import { join as join23, dirname as dirname8, basename as basename8 } from "path";
5604
+ import { join as join23, dirname as dirname9, basename as basename9 } from "path";
5406
5605
  import { createHash as createHash4 } from "crypto";
5407
5606
 
5408
5607
  // src/state.ts
@@ -5908,7 +6107,7 @@ var State = class _State {
5908
6107
  };
5909
6108
 
5910
6109
  // src/environment.ts
5911
- import { basename as basename5 } from "path";
6110
+ import { basename as basename6 } from "path";
5912
6111
  var API_KEY_VAR = "CREW_API_KEY";
5913
6112
  function authorshipBullets(a, contract) {
5914
6113
  if (!a) return [];
@@ -5961,7 +6160,7 @@ function repoSection(r, key, heading) {
5961
6160
  const repo = r.config;
5962
6161
  const branchTemplate = effectiveBranchTemplate(repo, repoPrefix(repo));
5963
6162
  const branchName = branchExample(repo, key);
5964
- const checkoutName = basename5(r.dir.replace(/[/\\]+$/, ""));
6163
+ const checkoutName = basename6(r.dir.replace(/[/\\]+$/, ""));
5965
6164
  const worktreeName = effectiveWorktreeDirName(
5966
6165
  repo,
5967
6166
  r.dir,
@@ -6848,7 +7047,7 @@ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as re
6848
7047
  import { join as join19 } from "path";
6849
7048
 
6850
7049
  // src/ticket-branch.ts
6851
- import { basename as basename6 } from "path";
7050
+ import { basename as basename7 } from "path";
6852
7051
 
6853
7052
  // src/tracker.ts
6854
7053
  import { hostname } from "os";
@@ -7866,7 +8065,7 @@ function worktreeForTicket(dir, cfg, t, role) {
7866
8065
  const number = t.issue_id.replace(/^\D+/, "");
7867
8066
  const { tag } = ticketBranchContext(t);
7868
8067
  return effectiveWorktreeDirName(cfg, dir, branchName, number, (template) => renderWorktreeName(template, {
7869
- dir: basename6(dir.replace(/[/\\]+$/, "")),
8068
+ dir: basename7(dir.replace(/[/\\]+$/, "")),
7870
8069
  key: t.issue_id,
7871
8070
  title: t.title ?? void 0,
7872
8071
  role,
@@ -9433,9 +9632,9 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
9433
9632
 
9434
9633
  // src/runner.ts
9435
9634
  import { spawn as spawn2, execFile, execFileSync as execFileSync4 } from "child_process";
9436
- import { mkdtempSync, writeFileSync as writeFileSync13, rmSync as rmSync12 } from "fs";
9635
+ import { mkdtempSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13, rmSync as rmSync12 } from "fs";
9437
9636
  import { tmpdir as tmpdir2 } from "os";
9438
- import { basename as basename7, join as join22 } from "path";
9637
+ import { basename as basename8, dirname as dirname8, join as join22 } from "path";
9439
9638
  import { promisify } from "util";
9440
9639
 
9441
9640
  // src/run-limit.ts
@@ -9555,8 +9754,20 @@ var SandboxRunner = class {
9555
9754
  if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0 && !secretKeys.includes(cfg.forgeTokenVar)) secretKeys.push(cfg.forgeTokenVar);
9556
9755
  const createEnv = { ...process.env, ...plan.setEnv };
9557
9756
  if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0) createEnv[cfg.forgeTokenVar] = o.env[cfg.forgeTokenVar];
9757
+ let policyFile = cfg.policy;
9758
+ let generated;
9759
+ if (!policyFile && plan.sandboxPolicy) {
9760
+ try {
9761
+ generated = generateSandboxPolicy({ mirrorDir: cfg.mirrorDir, workDir: cfg.workDir, ...plan.sandboxPolicy });
9762
+ } catch (e) {
9763
+ throw new Error(`cannot generate the sandbox policy: ${e.message}`);
9764
+ }
9765
+ policyFile = policyPathFor(plan.streamPath);
9766
+ mkdirSync12(dirname8(policyFile), { recursive: true });
9767
+ writeFileSync13(policyFile, generated.yaml, { mode: 384 });
9768
+ }
9558
9769
  const create2 = ["sandbox", "create", "--name", name, "--detach"];
9559
- if (cfg.policy) create2.push("--policy", cfg.policy);
9770
+ if (policyFile) create2.push("--policy", policyFile);
9560
9771
  if (cfg.image) create2.push("--from", cfg.image);
9561
9772
  if (cfg.cacheVolume) {
9562
9773
  create2.push("--driver-config-json", JSON.stringify({
@@ -9575,6 +9786,13 @@ var SandboxRunner = class {
9575
9786
  }
9576
9787
  o.emit.emit(`sandbox_created: ${name}`, { role: plan.role, data: { event: "sandbox_created", sandbox: name, ms: Date.now() - t0 } });
9577
9788
  const handle = { runner: "sandbox", sandbox: name, sandboxBin: cfg.bin };
9789
+ if (generated) {
9790
+ const hosts = generated.groups.flatMap((g) => g.endpoints.map((e) => `${e.host}:${e.port}`));
9791
+ o.emit.emit(`sandbox_policy: ${hosts.length} allowed endpoint(s)${generated.extraHosts.length ? `; declared by the repository: ${generated.extraHosts.join(", ")}` : ""}`, {
9792
+ role: plan.role,
9793
+ data: { event: "sandbox_policy", sandbox: name, file: policyFile, allowed: hosts, declared: generated.extraHosts }
9794
+ });
9795
+ }
9578
9796
  try {
9579
9797
  const branch = `crew-run-${name.slice(-8)}`;
9580
9798
  const clone = cloneScript({
@@ -9586,7 +9804,7 @@ var SandboxRunner = class {
9586
9804
  lockfile: cfg.depsLockfile ?? DEFAULT_CACHE_SETTINGS.depsLockfile
9587
9805
  });
9588
9806
  await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
9589
- const harness = cfg.agentBin ?? basename7(plan.bin);
9807
+ const harness = cfg.agentBin ?? basename8(plan.bin);
9590
9808
  const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
9591
9809
  if (viaUpload) {
9592
9810
  const tmp = mkdtempSync(join22(tmpdir2(), "crew-prompt-"));
@@ -9622,10 +9840,28 @@ var SandboxRunner = class {
9622
9840
  }
9623
9841
  async release(handle, emit) {
9624
9842
  if (!handle.sandbox) return;
9843
+ await this.foldDenied(handle.sandbox, handle.sandboxBin, emit);
9625
9844
  const t0 = Date.now();
9626
9845
  await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
9627
9846
  emit.emit(`sandbox_deleted: ${handle.sandbox}`, { data: { event: "sandbox_deleted", sandbox: handle.sandbox, ms: Date.now() - t0 } });
9628
9847
  }
9848
+ /**
9849
+ * Denied requests from the sandbox log become `sandbox_denied` events in the
9850
+ * run's event stream (CREW-2801), so an agent blocked by policy is diagnosable
9851
+ * from the Mac app. Read before the delete, which takes the log with it.
9852
+ */
9853
+ async foldDenied(name, bin, emit) {
9854
+ let log;
9855
+ try {
9856
+ log = (await execFileP(bin ?? this.cfg.bin, ["logs", name], { timeout: 15e3, maxBuffer: 16 * 1024 * 1024 })).stdout;
9857
+ } catch {
9858
+ return;
9859
+ }
9860
+ for (const d of parseDeniedRequests(log)) {
9861
+ const where = `${d.host}${d.port !== void 0 ? `:${d.port}` : ""}${d.binary ? ` by ${d.binary}` : ""}`;
9862
+ emit.warn(`sandbox_denied: ${where}${d.count > 1 ? ` (${d.count}x)` : ""} \u2014 blocked by the sandbox policy`, { data: { event: "sandbox_denied", sandbox: name, ...d } });
9863
+ }
9864
+ }
9629
9865
  async deleteQuietly(name, bin = this.cfg.bin) {
9630
9866
  try {
9631
9867
  await execFileP(bin, ["sandbox", "delete", name], { timeout: 6e4 });
@@ -9687,6 +9923,26 @@ function assemblePrompt(p) {
9687
9923
  }
9688
9924
  var AgentError = class extends Error {
9689
9925
  };
9926
+ function sandboxPolicyInputFor(route2, toolchain, o) {
9927
+ const repos = reposOf(route2);
9928
+ const forgeHosts = [...new Set(repos.map((r) => r.forgeHost ?? "github.com"))];
9929
+ const dir = repos.find((r) => r.name === o.repo)?.dir ?? route2.dir;
9930
+ let declared = null;
9931
+ try {
9932
+ declared = loadRepoConfig(dir);
9933
+ } catch {
9934
+ }
9935
+ return {
9936
+ forgeHosts,
9937
+ tablationUrl: route2.baseUrl,
9938
+ authMode: toolchain.auth ?? "inherit",
9939
+ provider: toolchainProvider(toolchain),
9940
+ harnessBin: o.sandbox?.agentBin ?? o.harnessBin,
9941
+ ...declared?.sandboxNetwork ? { extraHosts: declared.sandboxNetwork.hosts, toolchains: declared.sandboxNetwork.toolchains } : {},
9942
+ ...o.sandbox?.mirrorDir ? { mirrorDir: o.sandbox.mirrorDir } : {},
9943
+ ...o.sandbox?.workDir ? { workDir: o.sandbox.workDir } : {}
9944
+ };
9945
+ }
9690
9946
  var AgentSpawnError = class extends AgentError {
9691
9947
  };
9692
9948
  var WorkspaceAuthRefused = class extends AgentSpawnError {
@@ -9822,7 +10078,11 @@ function planAgentRun(o) {
9822
10078
  workspaceAuth: toolchain.auth === "workspace",
9823
10079
  provider: toolchainProvider(toolchain),
9824
10080
  maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
9825
- ...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
10081
+ ...o.ship.runner === "sandbox" ? {
10082
+ runner: "sandbox",
10083
+ ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {},
10084
+ sandboxPolicy: sandboxPolicyInputFor(o.route, toolchain, { harnessBin: invocation.bin, repo: o.repo, sandbox: o.ship.sandbox })
10085
+ } : {},
9826
10086
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
9827
10087
  warnings: [...seat.warnings ?? [], ...expanded.warnings],
9828
10088
  prompt,
@@ -9926,7 +10186,7 @@ async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMi
9926
10186
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
9927
10187
  function openSink(path) {
9928
10188
  try {
9929
- mkdirSync12(dirname8(path), { recursive: true });
10189
+ mkdirSync13(dirname9(path), { recursive: true });
9930
10190
  return createWriteStream(path, { flags: "a" });
9931
10191
  } catch {
9932
10192
  return void 0;
@@ -10215,7 +10475,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10215
10475
  if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
10216
10476
  if (sent.counterRetry) {
10217
10477
  try {
10218
- queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
10478
+ queueTicketCounters(stateDirOfStream(id.streamPath), basename9(id.streamPath, ".jsonl"), {
10219
10479
  version: 1,
10220
10480
  route: id.route,
10221
10481
  role: id.role,
@@ -10230,7 +10490,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
10230
10490
  } catch (e) {
10231
10491
  if (e instanceof AgentLogUnreachableError) {
10232
10492
  try {
10233
- queueAgentRun(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
10493
+ queueAgentRun(stateDirOfStream(id.streamPath), basename9(id.streamPath, ".jsonl"), {
10234
10494
  version: 1,
10235
10495
  route: id.route,
10236
10496
  role: id.role,
@@ -10268,7 +10528,7 @@ function spawnAgent(plan, emit, hintMoved) {
10268
10528
  const id = identityOf(plan, adapter.harness);
10269
10529
  let streamFd;
10270
10530
  try {
10271
- mkdirSync12(dirname8(plan.streamPath), { recursive: true });
10531
+ mkdirSync13(dirname9(plan.streamPath), { recursive: true });
10272
10532
  streamFd = openSync3(plan.streamPath, "a");
10273
10533
  } catch (e) {
10274
10534
  void endSink(eventsSink);
@@ -10507,7 +10767,7 @@ async function reconcileRuns(o) {
10507
10767
  }
10508
10768
 
10509
10769
  // src/live-runs.ts
10510
- import { existsSync as existsSync23, readdirSync as readdirSync10 } from "fs";
10770
+ import { existsSync as existsSync23, readdirSync as readdirSync10, statSync as statSync8 } from "fs";
10511
10771
  import { join as join25 } from "path";
10512
10772
  function liveRuns(stateDir, isAlive = runIsAlive) {
10513
10773
  const dir = join25(stateDir, "streams");
@@ -10523,6 +10783,23 @@ function liveRuns(stateDir, isAlive = runIsAlive) {
10523
10783
  }
10524
10784
  return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
10525
10785
  }
10786
+ function runActivity(stateDir, isAlive = runIsAlive) {
10787
+ const activeRuns = liveRuns(stateDir, isAlive).length;
10788
+ const dir = join25(stateDir, "streams");
10789
+ let newest = 0;
10790
+ if (existsSync23(dir)) {
10791
+ for (const name of readdirSync10(dir)) {
10792
+ if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
10793
+ const streamPath = join25(dir, name);
10794
+ if (!runFinished(streamPath)) continue;
10795
+ try {
10796
+ newest = Math.max(newest, statSync8(`${streamPath}.meta.json`).mtimeMs);
10797
+ } catch {
10798
+ }
10799
+ }
10800
+ }
10801
+ return { activeRuns, lastRunEndedAt: newest ? new Date(newest).toISOString() : null };
10802
+ }
10526
10803
  function siblingRuns(runs, ownTicketRecordId) {
10527
10804
  return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
10528
10805
  }
@@ -11543,7 +11820,7 @@ function readSandboxState(stateDir) {
11543
11820
  }
11544
11821
  }
11545
11822
  function writeSandboxState(stateDir, state) {
11546
- mkdirSync13(stateDir, { recursive: true });
11823
+ mkdirSync14(stateDir, { recursive: true });
11547
11824
  const file = sandboxStatePath(stateDir);
11548
11825
  writeFileSync15(`${file}.tmp`, `${JSON.stringify(state, null, 2)}
11549
11826
  `);
@@ -11596,17 +11873,17 @@ async function buildSandboxImage(plan, deps) {
11596
11873
  return { kind: "reused", tag: plan.tag };
11597
11874
  }
11598
11875
  const logDir = sandboxLogDir(deps.stateDir);
11599
- mkdirSync13(logDir, { recursive: true });
11876
+ mkdirSync14(logDir, { recursive: true });
11600
11877
  const logPath = join26(logDir, `${slug(plan.repo)}-${now().toISOString().replace(/[:.]/g, "-")}.log`);
11601
11878
  const builtLayers = [];
11602
11879
  for (const layer of plan.layers) {
11603
11880
  if (deps.hasImage(layer.tag)) continue;
11604
11881
  const ctx = join26(tmpdir3(), `crew-sandbox-${hash([layer.tag, process.pid, Date.now()])}`);
11605
- mkdirSync13(ctx, { recursive: true });
11882
+ mkdirSync14(ctx, { recursive: true });
11606
11883
  try {
11607
11884
  writeFileSync15(join26(ctx, "Dockerfile"), layer.dockerfile);
11608
11885
  for (const [name, content] of Object.entries(layer.files)) {
11609
- mkdirSync13(dirname9(join26(ctx, name)), { recursive: true });
11886
+ mkdirSync14(dirname10(join26(ctx, name)), { recursive: true });
11610
11887
  writeFileSync15(join26(ctx, name), content);
11611
11888
  }
11612
11889
  const code = await deps.run(["build", "--tag", layer.tag, "--file", join26(ctx, "Dockerfile"), ctx], logPath);
@@ -11787,7 +12064,7 @@ function roleCapWarnings(ship) {
11787
12064
 
11788
12065
  // src/selftest.ts
11789
12066
  import { spawn as spawn5 } from "child_process";
11790
- import { existsSync as existsSync25, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, readFileSync as readFileSync22, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
12067
+ import { existsSync as existsSync25, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, readFileSync as readFileSync22, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
11791
12068
  import { tmpdir as tmpdir4 } from "os";
11792
12069
  import { join as join27 } from "path";
11793
12070
  var SELFTEST_TIMEOUT_MS = 12e4;
@@ -11995,7 +12272,7 @@ function readSelftest(stateDir) {
11995
12272
  function recordSelftest(stateDir, results) {
11996
12273
  const all = readSelftest(stateDir);
11997
12274
  for (const r of results) all[r.toolchain] = r;
11998
- mkdirSync14(stateDir, { recursive: true });
12275
+ mkdirSync15(stateDir, { recursive: true });
11999
12276
  writeFileSync16(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
12000
12277
  `);
12001
12278
  }
@@ -12008,7 +12285,7 @@ function doctorSelftestLines(stored, toolchains) {
12008
12285
  }
12009
12286
 
12010
12287
  // src/cli.ts
12011
- import { resolve as resolve6, dirname as dirname27, join as join49, basename as basename15 } from "path";
12288
+ import { resolve as resolve6, dirname as dirname28, join as join49, basename as basename16 } from "path";
12012
12289
  import { fileURLToPath } from "url";
12013
12290
  import { hostname as hostname4, homedir as homedir11 } from "os";
12014
12291
  import { spawnSync as spawnSync10 } from "child_process";
@@ -12205,8 +12482,8 @@ function rosterMarkdown(roster, meId) {
12205
12482
  }
12206
12483
 
12207
12484
  // src/connect-resume.ts
12208
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync23, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
12209
- import { dirname as dirname10, join as join28 } from "path";
12485
+ import { mkdirSync as mkdirSync16, readFileSync as readFileSync23, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
12486
+ import { dirname as dirname11, join as join28 } from "path";
12210
12487
  function lastSessionFile(stateDir) {
12211
12488
  return join28(stateDir, "connect-session.json");
12212
12489
  }
@@ -12226,7 +12503,7 @@ function readPointers(stateDir) {
12226
12503
  }
12227
12504
  function writePointers(stateDir, pointers) {
12228
12505
  const file = lastSessionFile(stateDir);
12229
- mkdirSync15(dirname10(file), { recursive: true });
12506
+ mkdirSync16(dirname11(file), { recursive: true });
12230
12507
  writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
12231
12508
  `, { mode: 384 });
12232
12509
  }
@@ -12312,8 +12589,8 @@ async function signOutSessions(o) {
12312
12589
  }
12313
12590
 
12314
12591
  // src/reset.ts
12315
- import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
12316
- import { dirname as dirname11, join as join29 } from "path";
12592
+ import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync9, writeFileSync as writeFileSync18 } from "fs";
12593
+ import { dirname as dirname12, join as join29 } from "path";
12317
12594
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
12318
12595
  function clonedCheckoutsFile(stateDir) {
12319
12596
  return join29(stateDir, "cloned-checkouts.json");
@@ -12330,7 +12607,7 @@ function recordClonedCheckout(stateDir, dir) {
12330
12607
  const known = readClonedCheckouts(stateDir);
12331
12608
  if (known.includes(dir)) return;
12332
12609
  const file = clonedCheckoutsFile(stateDir);
12333
- mkdirSync16(dirname11(file), { recursive: true });
12610
+ mkdirSync17(dirname12(file), { recursive: true });
12334
12611
  writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
12335
12612
  `);
12336
12613
  }
@@ -12410,7 +12687,7 @@ function removeState(stateDir, dryRun) {
12410
12687
  const targets = [join29(stateDir, "resolved"), join29(stateDir, "keys"), clonedCheckoutsFile(stateDir), join29(stateDir, "connect-session.json"), join29(stateDir, "reload-requested"), join29(stateDir, "daemon.started")];
12411
12688
  try {
12412
12689
  for (const name of readdirSync12(stateDir)) {
12413
- if (name.startsWith(".") && statSync8(join29(stateDir, name)).isFile()) targets.push(join29(stateDir, name));
12690
+ if (name.startsWith(".") && statSync9(join29(stateDir, name)).isFile()) targets.push(join29(stateDir, name));
12414
12691
  }
12415
12692
  } catch {
12416
12693
  }
@@ -12511,9 +12788,9 @@ async function runDeprovision(o, fx, deps) {
12511
12788
  }
12512
12789
 
12513
12790
  // src/pool-member.ts
12514
- import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
12791
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
12515
12792
  import { homedir as homedir8 } from "os";
12516
- import { dirname as dirname12, join as join30 } from "path";
12793
+ import { dirname as dirname13, join as join30 } from "path";
12517
12794
  var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
12518
12795
  function backoffMs(attempt) {
12519
12796
  return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
@@ -12538,6 +12815,7 @@ async function runPoolMember(deps) {
12538
12815
  }
12539
12816
  };
12540
12817
  let reported;
12818
+ let busy = false;
12541
12819
  const report = async (state, o = {}) => {
12542
12820
  const body = {
12543
12821
  state,
@@ -12547,6 +12825,12 @@ async function runPoolMember(deps) {
12547
12825
  reason: o.reason ?? null,
12548
12826
  crewVersion: deps.host.crewVersion
12549
12827
  };
12828
+ const act = state === "running" ? deps.activity?.() : void 0;
12829
+ if (act) {
12830
+ body.activeRuns = act.activeRuns;
12831
+ body.lastRunEndedAt = act.lastRunEndedAt;
12832
+ }
12833
+ if (act) busy = act.activeRuns > 0;
12550
12834
  const r = await settle("status", () => deps.api.status(body));
12551
12835
  if (!r) return "stopped";
12552
12836
  if (r.status === 401) return "credential";
@@ -12642,7 +12926,25 @@ async function runPoolMember(deps) {
12642
12926
  if (!current) await deps.maybeSelfUpgrade?.().catch((e) => {
12643
12927
  deps.log(`self-upgrade: ${e.message}`);
12644
12928
  });
12645
- const ans = await settle("lease", () => deps.api.lease(platform2.pollWaitSeconds || DEFAULT_POLL_WAIT_SECONDS, current ?? void 0));
12929
+ let stopWatch;
12930
+ let flipping = false;
12931
+ if (current && deps.watch && deps.activity) {
12932
+ const held = current;
12933
+ stopWatch = deps.watch(() => {
12934
+ const now = deps.activity().activeRuns > 0;
12935
+ if (flipping || now === busy) return;
12936
+ flipping = true;
12937
+ void report("running", { leaseId: held }).catch(() => void 0).finally(() => {
12938
+ flipping = false;
12939
+ });
12940
+ });
12941
+ }
12942
+ let ans;
12943
+ try {
12944
+ ans = await settle("lease", () => deps.api.lease(platform2.pollWaitSeconds || DEFAULT_POLL_WAIT_SECONDS, current ?? void 0));
12945
+ } finally {
12946
+ stopWatch?.();
12947
+ }
12646
12948
  if (!ans) return POOL_EXIT.ok;
12647
12949
  if (ans.status === 401) {
12648
12950
  deps.log("the pool credential is unknown or revoked (401)");
@@ -12655,10 +12957,10 @@ async function runPoolMember(deps) {
12655
12957
  }
12656
12958
  if (ans.status === 204) {
12657
12959
  const url = current ? deps.relayUrl() : null;
12658
- if (current && url && url !== relayReported) {
12960
+ if (current && (url !== relayReported && url || deps.activity)) {
12659
12961
  const r2 = await report("running", { leaseId: current });
12660
12962
  if (r2 === "credential") return POOL_EXIT.credential;
12661
- relayReported = url;
12963
+ relayReported = url ?? relayReported;
12662
12964
  }
12663
12965
  continue;
12664
12966
  }
@@ -12715,7 +13017,7 @@ function fileMemberStore(dir) {
12715
13017
  }
12716
13018
  },
12717
13019
  write(f) {
12718
- mkdirSync17(dirname12(file), { recursive: true, mode: 448 });
13020
+ mkdirSync18(dirname13(file), { recursive: true, mode: 448 });
12719
13021
  const tmp = `${file}.tmp`;
12720
13022
  writeFileSync19(tmp, `${JSON.stringify(f)}
12721
13023
  `, { mode: 384 });
@@ -12775,8 +13077,8 @@ import { hostname as hostname3 } from "os";
12775
13077
  // src/machine-id.ts
12776
13078
  import { execFileSync as execFileSync8 } from "child_process";
12777
13079
  import { createHash as createHash7 } from "crypto";
12778
- import { existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
12779
- import { dirname as dirname13, join as join31 } from "path";
13080
+ import { existsSync as existsSync27, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
13081
+ import { dirname as dirname14, join as join31 } from "path";
12780
13082
  function machineIdPathFor(stateDir) {
12781
13083
  return join31(stateDir, "machine-id");
12782
13084
  }
@@ -12810,7 +13112,7 @@ function ensureMachineId(stateDir, opts = {}) {
12810
13112
  const raw = (opts.read ?? readRawMachineId)();
12811
13113
  if (!raw) return void 0;
12812
13114
  const hashed = sha256(raw);
12813
- mkdirSync18(dirname13(path), { recursive: true });
13115
+ mkdirSync19(dirname14(path), { recursive: true });
12814
13116
  writeFileSync20(path, `${hashed}
12815
13117
  `, { mode: 384 });
12816
13118
  return hashed;
@@ -12833,8 +13135,8 @@ import { parseDocument as parseDocument2 } from "yaml";
12833
13135
 
12834
13136
  // src/repos-cmd.ts
12835
13137
  import { homedir as homedir9 } from "os";
12836
- import { basename as basename9, join as join32, resolve as resolve3 } from "path";
12837
- import { isMap, isSeq, parseDocument, stringify } from "yaml";
13138
+ import { basename as basename10, join as join32, resolve as resolve3 } from "path";
13139
+ import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
12838
13140
  var ReposError = class extends Error {
12839
13141
  };
12840
13142
  function normalizeRemote(url) {
@@ -12905,7 +13207,7 @@ function configuredRepos(text2, route2, base) {
12905
13207
  });
12906
13208
  }
12907
13209
  const dir = m.get("dir");
12908
- return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
13210
+ return dir ? [{ name: basename10(String(dir)), dir: expandDir(String(dir), base) }] : [];
12909
13211
  }
12910
13212
  function cloneBaseOf(text2, route2, base, isCheckout) {
12911
13213
  const m = routeMapOf(parseDocument(text2), route2);
@@ -12994,7 +13296,7 @@ function planRepoAdd(i) {
12994
13296
  throw new ReposError(`${target} is already configured on route ${i.route}`);
12995
13297
  }
12996
13298
  const matched = matchRepoName(i.origin, i.resolved);
12997
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
13299
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename10(target);
12998
13300
  let warning;
12999
13301
  if (!matched) {
13000
13302
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -13047,7 +13349,7 @@ function planRepoRemove(text2, base, route2, path) {
13047
13349
  return { text: String(doc), name: hit.name };
13048
13350
  }
13049
13351
  function renderBlock(m) {
13050
- return stringify([m.toJSON()]).trimEnd();
13352
+ return stringify2([m.toJSON()]).trimEnd();
13051
13353
  }
13052
13354
  var LOCAL_RELEASE_MODES = /* @__PURE__ */ new Set(["integrate", "external"]);
13053
13355
  function planRepoRow(i) {
@@ -13454,7 +13756,7 @@ host=${host}
13454
13756
 
13455
13757
  // src/tunnel.ts
13456
13758
  import { spawn as spawn6 } from "child_process";
13457
- import { existsSync as existsSync29, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
13759
+ import { existsSync as existsSync29, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
13458
13760
  import { randomInt } from "crypto";
13459
13761
  import { join as join33 } from "path";
13460
13762
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -13541,7 +13843,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
13541
13843
  }
13542
13844
  function writePersistedTunnel(stateDir, state) {
13543
13845
  const path = tunnelStatePath(stateDir, state.workspaceId);
13544
- mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
13846
+ mkdirSync20(join33(stateDir, "passengers"), { recursive: true });
13545
13847
  writeFileSync22(path, `${JSON.stringify(state, null, 2)}
13546
13848
  `);
13547
13849
  }
@@ -13732,6 +14034,16 @@ function realPoolMemberDeps(o) {
13732
14034
  }
13733
14035
  return null;
13734
14036
  },
14037
+ activity() {
14038
+ const cfg = loadShip(o.crewHome);
14039
+ return cfg ? runActivity(cfg.ship.stateDir) : { activeRuns: 0, lastRunEndedAt: null };
14040
+ },
14041
+ watch(onTick) {
14042
+ const t = setInterval(onTick, 2e3);
14043
+ return () => {
14044
+ clearInterval(t);
14045
+ };
14046
+ },
13735
14047
  sleep: (ms3) => new Promise((r) => setTimeout(r, ms3)),
13736
14048
  log: o.log,
13737
14049
  stopping: o.stopping
@@ -15005,8 +15317,8 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
15005
15317
  }
15006
15318
 
15007
15319
  // src/repo-refresh.ts
15008
- import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
15009
- import { dirname as dirname15 } from "path";
15320
+ import { existsSync as existsSync31, mkdirSync as mkdirSync21, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
15321
+ import { dirname as dirname16 } from "path";
15010
15322
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
15011
15323
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
15012
15324
  var lastAsked = /* @__PURE__ */ new Map();
@@ -15038,7 +15350,7 @@ function persistRepoRows(stateDir, routeName, rows) {
15038
15350
  if (!existsSync31(path)) return;
15039
15351
  const raw = JSON.parse(readFileSync29(path, "utf8"));
15040
15352
  applyRepoRows(raw, rows);
15041
- mkdirSync20(dirname15(path), { recursive: true });
15353
+ mkdirSync21(dirname16(path), { recursive: true });
15042
15354
  writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
15043
15355
  `);
15044
15356
  }
@@ -15489,9 +15801,9 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
15489
15801
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
15490
15802
 
15491
15803
  // src/connect-wizard.ts
15492
- import { delimiter, dirname as dirname16, join as join35 } from "path";
15493
- import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync25 } from "fs";
15494
- import { stringify as stringify2 } from "yaml";
15804
+ import { delimiter, dirname as dirname17, join as join35 } from "path";
15805
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync22, writeFileSync as writeFileSync25 } from "fs";
15806
+ import { stringify as stringify3 } from "yaml";
15495
15807
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
15496
15808
  function shouldRunWizard(opts) {
15497
15809
  if (!opts.isTTY) return false;
@@ -15538,9 +15850,9 @@ async function runWizard(p, env) {
15538
15850
  if (entered && isExec(entered)) agentBin = entered;
15539
15851
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
15540
15852
  }
15541
- const nodeDir = dirname16(env.nodePath);
15853
+ const nodeDir = dirname17(env.nodePath);
15542
15854
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
15543
- const baseDirs = [nodeDir, pnpmPath ? dirname16(pnpmPath) : void 0];
15855
+ const baseDirs = [nodeDir, pnpmPath ? dirname17(pnpmPath) : void 0];
15544
15856
  p.say(
15545
15857
  `
15546
15858
  Host Passengers lets this machine run the agent sessions that other people's
@@ -15551,14 +15863,14 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
15551
15863
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
15552
15864
  }
15553
15865
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
15554
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname16(dockerPath) : void 0]);
15866
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname17(dockerPath) : void 0]);
15555
15867
  if (extraPath) {
15556
15868
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
15557
15869
  }
15558
15870
  return { name, platform: env.platform, agentBin, extraPath, hostPassengers };
15559
15871
  }
15560
15872
  function renderShipBlock(a) {
15561
- return stringify2({
15873
+ return stringify3({
15562
15874
  ship: {
15563
15875
  name: a.name,
15564
15876
  platform: a.platform,
@@ -15573,7 +15885,7 @@ routes:
15573
15885
  ${routeBlock}`;
15574
15886
  }
15575
15887
  function writeNewConfig(path, text2) {
15576
- mkdirSync21(dirname16(path), { recursive: true });
15888
+ mkdirSync22(dirname17(path), { recursive: true });
15577
15889
  try {
15578
15890
  writeFileSync25(path, text2, { mode: 384, flag: "wx" });
15579
15891
  return true;
@@ -15609,7 +15921,7 @@ Wrote ${o.configPath}.
15609
15921
  // src/connect-json.ts
15610
15922
  import { accessSync as accessSync4, constants as constants4 } from "fs";
15611
15923
  import { homedir as homedir10 } from "os";
15612
- import { dirname as dirname17, join as join36 } from "path";
15924
+ import { dirname as dirname18, join as join36 } from "path";
15613
15925
  import { delimiter as delimiter2 } from "path";
15614
15926
  var CHOICE_EXIT_CODE = 3;
15615
15927
  function createEmitter(json2, write) {
@@ -15675,7 +15987,7 @@ function flagWizardAnswers(f, env) {
15675
15987
  const agentBin = f.agentBin ?? found?.bin;
15676
15988
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
15677
15989
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
15678
- const extraPath = f.extraPath ?? deriveExtraPath([dirname17(env.nodePath), pnpmPath ? dirname17(pnpmPath) : void 0, dockerPath ? dirname17(dockerPath) : void 0]);
15990
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname18(env.nodePath), pnpmPath ? dirname18(pnpmPath) : void 0, dockerPath ? dirname18(dockerPath) : void 0]);
15679
15991
  return {
15680
15992
  answers: {
15681
15993
  name: f.shipName?.trim() || env.hostname,
@@ -15689,8 +16001,8 @@ function flagWizardAnswers(f, env) {
15689
16001
  }
15690
16002
 
15691
16003
  // src/release-run.ts
15692
- import { readFileSync as readFileSync30, writeFileSync as writeFileSync26, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, rmSync as rmSync17 } from "fs";
15693
- import { dirname as dirname18, join as join37 } from "path";
16004
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync26, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync23, mkdtempSync as mkdtempSync3, rmSync as rmSync17 } from "fs";
16005
+ import { dirname as dirname19, join as join37 } from "path";
15694
16006
  import { tmpdir as tmpdir5 } from "os";
15695
16007
 
15696
16008
  // src/release.ts
@@ -16576,7 +16888,7 @@ function openReleaseCheckout(o) {
16576
16888
  for (const f of o.repo.worktrees.copy) {
16577
16889
  const src = join37(o.cwd, f);
16578
16890
  if (!existsSync33(src)) continue;
16579
- mkdirSync22(dirname18(join37(dir, f)), { recursive: true });
16891
+ mkdirSync23(dirname19(join37(dir, f)), { recursive: true });
16580
16892
  copyFileSync(src, join37(dir, f));
16581
16893
  }
16582
16894
  return { dir, branch, close };
@@ -17551,7 +17863,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
17551
17863
  }
17552
17864
 
17553
17865
  // src/watch.ts
17554
- import { watch as watchFile, existsSync as existsSync34, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
17866
+ import { watch as watchFile, existsSync as existsSync34, statSync as statSync10, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
17555
17867
  var E = "\x1B[";
17556
17868
  var dim = (s) => `${E}2m${s}${E}0m`;
17557
17869
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -17653,7 +17965,7 @@ function renderHeaders(states, now = Date.now()) {
17653
17965
  }
17654
17966
  function readFrom(file, from) {
17655
17967
  if (!existsSync34(file)) return { events: [], offset: from };
17656
- const size = statSync9(file).size;
17968
+ const size = statSync10(file).size;
17657
17969
  if (size < from) return readFrom(file, 0);
17658
17970
  if (size === from) return { events: [], offset: from };
17659
17971
  const fd = openSync4(file, "r");
@@ -17895,8 +18207,8 @@ function nextRoles(decision, ran) {
17895
18207
  }
17896
18208
 
17897
18209
  // src/ship-name.ts
17898
- import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
17899
- import { dirname as dirname19 } from "path";
18210
+ import { existsSync as existsSync35, mkdirSync as mkdirSync24, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
18211
+ import { dirname as dirname20 } from "path";
17900
18212
  import { parseDocument as parseDocument5 } from "yaml";
17901
18213
  var ShipNameError = class extends Error {
17902
18214
  };
@@ -17975,7 +18287,7 @@ function writeSyncedName(resolvedFile, name) {
17975
18287
  }
17976
18288
  if (raw.shipName === name) return;
17977
18289
  raw.shipName = name;
17978
- mkdirSync23(dirname19(resolvedFile), { recursive: true });
18290
+ mkdirSync24(dirname20(resolvedFile), { recursive: true });
17979
18291
  const tmp = `${resolvedFile}.tmp${process.pid}`;
17980
18292
  writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
17981
18293
  `);
@@ -18668,8 +18980,8 @@ function describeCapability(i) {
18668
18980
  }
18669
18981
 
18670
18982
  // src/agents-sync-run.ts
18671
- import { existsSync as existsSync36, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
18672
- import { dirname as dirname20 } from "path";
18983
+ import { existsSync as existsSync36, mkdirSync as mkdirSync25, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
18984
+ import { dirname as dirname21 } from "path";
18673
18985
  async function runAgentsSync(target, o) {
18674
18986
  const dryRun = o.dryRun ?? false;
18675
18987
  const force = o.force ?? false;
@@ -18706,7 +19018,7 @@ async function runAgentsSync(target, o) {
18706
19018
  raw.agentPersonas = personaResult.agentPersonas;
18707
19019
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
18708
19020
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
18709
- mkdirSync24(dirname20(resolvedPath), { recursive: true });
19021
+ mkdirSync25(dirname21(resolvedPath), { recursive: true });
18710
19022
  writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
18711
19023
  `);
18712
19024
  if (target.resolved) {
@@ -18812,11 +19124,11 @@ async function showLogEntry(route2, entryId, opts = {}) {
18812
19124
  // src/repos-check.ts
18813
19125
  import { spawnSync as spawnSync9 } from "child_process";
18814
19126
  import { readFileSync as readFileSync33 } from "fs";
18815
- import { basename as basename10 } from "path";
19127
+ import { basename as basename11 } from "path";
18816
19128
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
18817
19129
  function checkHookSyntax(script, shell) {
18818
19130
  const spec = resolveShell(shell);
18819
- const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
19131
+ const name = basename11(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18820
19132
  let run3;
18821
19133
  if (POSIX_SHELL.test(name)) {
18822
19134
  run3 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
@@ -18835,7 +19147,7 @@ function checkHookSyntax(script, shell) {
18835
19147
  return { status: "error", message };
18836
19148
  }
18837
19149
  function renderSamples(cfg, dir) {
18838
- const checkout = basename10(dir.replace(/[/\\]+$/, ""));
19150
+ const checkout = basename11(dir.replace(/[/\\]+$/, ""));
18839
19151
  const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
18840
19152
  const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
18841
19153
  return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
@@ -19299,7 +19611,7 @@ ${run3.err}` });
19299
19611
  }
19300
19612
 
19301
19613
  // src/sample-ticket.ts
19302
- import { basename as basename11, dirname as dirname21, join as join38 } from "path";
19614
+ import { basename as basename12, dirname as dirname22, join as join38 } from "path";
19303
19615
  var SAMPLE_REPO = "crew-sample";
19304
19616
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
19305
19617
  var SAMPLE_MARKER = "sample: true";
@@ -19322,7 +19634,7 @@ has something safe to work on: its test and build hooks always pass, it has no
19322
19634
  remote, and \`crew sample-ticket --remove\` deletes it.
19323
19635
  `;
19324
19636
  function sampleDir(existing, reposBase, workspace) {
19325
- if (existing.length > 0) return join38(dirname21(existing[0]), SAMPLE_REPO);
19637
+ if (existing.length > 0) return join38(dirname22(existing[0]), SAMPLE_REPO);
19326
19638
  return join38(reposBase, workspace, SAMPLE_REPO);
19327
19639
  }
19328
19640
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
@@ -19368,7 +19680,7 @@ function planSampleTicket(i) {
19368
19680
 
19369
19681
  // src/worktree-sweep.ts
19370
19682
  import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync14 } from "fs";
19371
- import { basename as basename12, dirname as dirname22, join as join39, resolve as resolve4 } from "path";
19683
+ import { basename as basename13, dirname as dirname23, join as join39, resolve as resolve4 } from "path";
19372
19684
  function planWorktreeSweep(target, tickets, contract) {
19373
19685
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
19374
19686
  const verified = contract.statuses.verified;
@@ -19409,7 +19721,7 @@ function ticketForWorktree(w, byNumber, contract) {
19409
19721
  const t = num ? byNumber.get(num) : void 0;
19410
19722
  if (t && ticketStems(t).has(stem)) return t;
19411
19723
  }
19412
- const dir = basename12(w.path);
19724
+ const dir = basename13(w.path);
19413
19725
  const m = DIR_NUMBER.exec(dir);
19414
19726
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
19415
19727
  if (!w.branch) {
@@ -19425,15 +19737,15 @@ function worktreeTicketNumbers(target) {
19425
19737
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
19426
19738
  if (n) nums.add(n);
19427
19739
  }
19428
- const m = DIR_NUMBER_SUFFIXED.exec(basename12(w.path));
19740
+ const m = DIR_NUMBER_SUFFIXED.exec(basename13(w.path));
19429
19741
  if (m) nums.add(m[1]);
19430
19742
  }
19431
19743
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
19432
19744
  return [...nums];
19433
19745
  }
19434
19746
  function siblingWorktreeDirs(target) {
19435
- const parent = dirname22(target.dir.replace(/[/\\]+$/, ""));
19436
- const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
19747
+ const parent = dirname23(target.dir.replace(/[/\\]+$/, ""));
19748
+ const stem = `${basename13(target.dir.replace(/[/\\]+$/, ""))}-`;
19437
19749
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
19438
19750
  let names;
19439
19751
  try {
@@ -19458,7 +19770,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
19458
19770
  }
19459
19771
  function warnOrphanedWorktreeDirs(paths, log, held) {
19460
19772
  for (const p of paths) {
19461
- const number = DIR_NUMBER_SUFFIXED.exec(basename12(p))?.[1];
19773
+ const number = DIR_NUMBER_SUFFIXED.exec(basename13(p))?.[1];
19462
19774
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
19463
19775
  if (when) {
19464
19776
  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" });
@@ -19602,7 +19914,7 @@ function isTicketBranch(name, base, stems) {
19602
19914
  }
19603
19915
  function sweepCandidatesExist(cwd, base) {
19604
19916
  const linked = worktrees(cwd).slice(1);
19605
- if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename12(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
19917
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename13(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
19606
19918
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
19607
19919
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
19608
19920
  }
@@ -19653,7 +19965,7 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
19653
19965
  }
19654
19966
 
19655
19967
  // src/stream-sweep.ts
19656
- import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync18, statSync as statSync10 } from "fs";
19968
+ import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync18, statSync as statSync11 } from "fs";
19657
19969
  import { join as join40 } from "path";
19658
19970
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
19659
19971
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
@@ -19662,7 +19974,7 @@ function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
19662
19974
  const cutoff = now - retentionDays * DAY_MS2;
19663
19975
  return readdirSync15(dir).map((name) => join40(dir, name)).filter((path) => {
19664
19976
  try {
19665
- return statSync10(path).mtimeMs < cutoff;
19977
+ return statSync11(path).mtimeMs < cutoff;
19666
19978
  } catch {
19667
19979
  return false;
19668
19980
  }
@@ -19690,7 +20002,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
19690
20002
  const cutoff = now - retentionDays * DAY_MS2;
19691
20003
  return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join40(stateDir, name)).filter((path) => {
19692
20004
  try {
19693
- return statSync10(path).mtimeMs < cutoff;
20005
+ return statSync11(path).mtimeMs < cutoff;
19694
20006
  } catch {
19695
20007
  return false;
19696
20008
  }
@@ -19698,11 +20010,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
19698
20010
  }
19699
20011
 
19700
20012
  // src/cli.ts
19701
- import { realpathSync as realpathSync3, readFileSync as readFileSync40, existsSync as existsSync45, mkdirSync as mkdirSync31, writeFileSync as writeFileSync33, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync14, readdirSync as readdirSync20 } from "fs";
20013
+ import { realpathSync as realpathSync3, readFileSync as readFileSync40, existsSync as existsSync45, mkdirSync as mkdirSync32, writeFileSync as writeFileSync33, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync15, readdirSync as readdirSync20 } from "fs";
19702
20014
  import { createInterface } from "readline/promises";
19703
20015
 
19704
20016
  // src/periscope-cmd.ts
19705
- import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
20017
+ import { existsSync as existsSync39, mkdirSync as mkdirSync26, readFileSync as readFileSync35, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
19706
20018
  import { join as join41 } from "path";
19707
20019
  var PeriscopeError = class extends Error {
19708
20020
  };
@@ -19720,7 +20032,7 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
19720
20032
  }
19721
20033
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
19722
20034
  try {
19723
- mkdirSync25(priorDir(stateDir), { recursive: true });
20035
+ mkdirSync26(priorDir(stateDir), { recursive: true });
19724
20036
  writeFileSync29(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
19725
20037
  } catch {
19726
20038
  }
@@ -20126,7 +20438,7 @@ ${tail2(preview.output)}`);
20126
20438
 
20127
20439
  // src/periscope-idle.ts
20128
20440
  import { execFileSync as execFileSync10 } from "child_process";
20129
- import { statSync as statSync11 } from "fs";
20441
+ import { statSync as statSync12 } from "fs";
20130
20442
  import { join as join42 } from "path";
20131
20443
  var MAX_SCANNED_FILES = 2e4;
20132
20444
  function newestWorktreeMtime(root) {
@@ -20147,7 +20459,7 @@ function newestWorktreeMtime(root) {
20147
20459
  if (!rel) continue;
20148
20460
  if (++scanned > MAX_SCANNED_FILES) break;
20149
20461
  try {
20150
- const m = statSync11(join42(root, rel)).mtimeMs;
20462
+ const m = statSync12(join42(root, rel)).mtimeMs;
20151
20463
  if (newest === null || m > newest) newest = m;
20152
20464
  } catch {
20153
20465
  }
@@ -20223,8 +20535,8 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
20223
20535
 
20224
20536
  // src/ssh-keys.ts
20225
20537
  import { execFileSync as execFileSync11 } from "child_process";
20226
- import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
20227
- import { dirname as dirname23, join as join43 } from "path";
20538
+ import { existsSync as existsSync40, mkdirSync as mkdirSync27, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
20539
+ import { dirname as dirname24, join as join43 } from "path";
20228
20540
  var SshKeyError = class extends Error {
20229
20541
  };
20230
20542
  function shipSshKeyPathFor(stateDir) {
@@ -20234,7 +20546,7 @@ function ensureShipSshKeypair(stateDir) {
20234
20546
  const privateKeyPath = shipSshKeyPathFor(stateDir);
20235
20547
  const publicKeyPath = `${privateKeyPath}.pub`;
20236
20548
  if (!existsSync40(privateKeyPath)) {
20237
- mkdirSync26(dirname23(privateKeyPath), { recursive: true });
20549
+ mkdirSync27(dirname24(privateKeyPath), { recursive: true });
20238
20550
  try {
20239
20551
  execFileSync11("ssh-keygen", [
20240
20552
  "-t",
@@ -20421,12 +20733,12 @@ async function unregisterEndpoint(workspaceId, info, d) {
20421
20733
  import { createServer as createHttpServer } from "http";
20422
20734
  import { spawn as spawn8 } from "child_process";
20423
20735
  import { randomBytes } from "crypto";
20424
- import { existsSync as existsSync41, mkdirSync as mkdirSync28, readFileSync as readFileSync37, writeFileSync as writeFileSync30, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
20736
+ import { existsSync as existsSync41, mkdirSync as mkdirSync29, readFileSync as readFileSync37, writeFileSync as writeFileSync30, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
20425
20737
  import { join as join44 } from "path";
20426
20738
 
20427
20739
  // src/passenger-mirror.ts
20428
- import { mkdirSync as mkdirSync27 } from "fs";
20429
- import { dirname as dirname24 } from "path";
20740
+ import { mkdirSync as mkdirSync28 } from "fs";
20741
+ import { dirname as dirname25 } from "path";
20430
20742
  function mirrorRepoOf(spec) {
20431
20743
  const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
20432
20744
  if (fromOrigin) return fromOrigin;
@@ -20444,9 +20756,9 @@ async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCa
20444
20756
  continue;
20445
20757
  }
20446
20758
  try {
20447
- const parent = dirname24(spec.dir);
20448
- mkdirSync27(parent, { recursive: true });
20449
- mkdirSync27(spec.dir, { recursive: true });
20759
+ const parent = dirname25(spec.dir);
20760
+ mkdirSync28(parent, { recursive: true });
20761
+ mkdirSync28(spec.dir, { recursive: true });
20450
20762
  const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
20451
20763
  out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
20452
20764
  } catch (e) {
@@ -20565,7 +20877,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
20565
20877
  }
20566
20878
  function writePersistedSyncDaemon(stateDir, state) {
20567
20879
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
20568
- mkdirSync28(join44(stateDir, "passengers"), { recursive: true });
20880
+ mkdirSync29(join44(stateDir, "passengers"), { recursive: true });
20569
20881
  writeFileSync30(path, `${JSON.stringify(state, null, 2)}
20570
20882
  `);
20571
20883
  }
@@ -20675,7 +20987,7 @@ function describeCause(err3) {
20675
20987
  }
20676
20988
 
20677
20989
  // src/daemon.ts
20678
- import { existsSync as existsSync42, mkdirSync as mkdirSync29, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync12, writeFileSync as writeFileSync31 } from "fs";
20990
+ import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync13, writeFileSync as writeFileSync31 } from "fs";
20679
20991
  import { join as join45 } from "path";
20680
20992
  import { randomBytes as randomBytes2 } from "crypto";
20681
20993
  var DEFAULT_FLOOR_MS = 5e3;
@@ -20756,7 +21068,7 @@ async function runDaemonLoop(o) {
20756
21068
  function latestMtime(path) {
20757
21069
  let stat2;
20758
21070
  try {
20759
- stat2 = statSync12(path);
21071
+ stat2 = statSync13(path);
20760
21072
  } catch {
20761
21073
  return 0;
20762
21074
  }
@@ -20780,7 +21092,7 @@ function reloadSentinelFor(stateDir) {
20780
21092
  }
20781
21093
  function readReloadRequestedAt(stateDir) {
20782
21094
  try {
20783
- return statSync12(reloadSentinelFor(stateDir)).mtimeMs;
21095
+ return statSync13(reloadSentinelFor(stateDir)).mtimeMs;
20784
21096
  } catch {
20785
21097
  return null;
20786
21098
  }
@@ -20789,7 +21101,7 @@ function daemonStampFor(stateDir) {
20789
21101
  return join45(stateDir, "daemon.started");
20790
21102
  }
20791
21103
  function noteDaemonStarted(stateDir, now = Date.now()) {
20792
- mkdirSync29(stateDir, { recursive: true });
21104
+ mkdirSync30(stateDir, { recursive: true });
20793
21105
  rmSync20(reloadSentinelFor(stateDir), { force: true });
20794
21106
  writeFileSync31(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
20795
21107
  `);
@@ -20805,7 +21117,7 @@ function readDaemonStarted(stateDir) {
20805
21117
  function configChangedAfter(files, startedAt) {
20806
21118
  for (const f of files) {
20807
21119
  try {
20808
- if (statSync12(f).mtimeMs > startedAt) return f;
21120
+ if (statSync13(f).mtimeMs > startedAt) return f;
20809
21121
  } catch {
20810
21122
  }
20811
21123
  }
@@ -21368,12 +21680,12 @@ import {
21368
21680
  readSync as readSync3,
21369
21681
  readdirSync as readdirSync18,
21370
21682
  rmSync as rmSync21,
21371
- statSync as statSync13,
21683
+ statSync as statSync14,
21372
21684
  watch
21373
21685
  } from "fs";
21374
21686
  import { createConnection } from "net";
21375
21687
  import { createServer } from "http";
21376
- import { basename as basename13, dirname as dirname25, join as join46 } from "path";
21688
+ import { basename as basename14, dirname as dirname26, join as join46 } from "path";
21377
21689
  import { execFile as execFile3, spawn as spawn10 } from "child_process";
21378
21690
 
21379
21691
  // src/periscope-control.ts
@@ -21689,7 +22001,7 @@ function candidates(live, rotated) {
21689
22001
  const out = [live];
21690
22002
  if (!rotated) return out;
21691
22003
  try {
21692
- for (const n of readdirSync18(dirname25(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname25(live), n));
22004
+ for (const n of readdirSync18(dirname26(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname26(live), n));
21693
22005
  } catch {
21694
22006
  }
21695
22007
  return out;
@@ -21712,7 +22024,7 @@ function readRange(path, from, max) {
21712
22024
  }
21713
22025
  }
21714
22026
  function lastLines2(path, n) {
21715
- const st = statSync13(path);
22027
+ const st = statSync14(path);
21716
22028
  const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
21717
22029
  if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
21718
22030
  let start = st.size;
@@ -21746,7 +22058,7 @@ function readTail(live, opts) {
21746
22058
  } else {
21747
22059
  const found = candidates(live, opts.rotated ?? false).find((p) => {
21748
22060
  try {
21749
- const s = statSync13(p);
22061
+ const s = statSync14(p);
21750
22062
  return s.ino === cur.ino && s.dev === cur.dev;
21751
22063
  } catch {
21752
22064
  return false;
@@ -21757,7 +22069,7 @@ function readTail(live, opts) {
21757
22069
  off = cur.off;
21758
22070
  } else reset = true;
21759
22071
  }
21760
- let size = statSync13(path).size;
22072
+ let size = statSync14(path).size;
21761
22073
  if (off > size) {
21762
22074
  off = 0;
21763
22075
  reset = true;
@@ -21765,12 +22077,12 @@ function readTail(live, opts) {
21765
22077
  if (off >= size && path !== live) {
21766
22078
  path = live;
21767
22079
  off = 0;
21768
- size = statSync13(path).size;
22080
+ size = statSync14(path).size;
21769
22081
  }
21770
22082
  const { text: text2 } = readRange(path, off, max);
21771
22083
  const cut = text2.lastIndexOf("\n");
21772
22084
  const whole = cut < 0 ? "" : text2.slice(0, cut + 1);
21773
- const st = statSync13(path);
22085
+ const st = statSync14(path);
21774
22086
  return {
21775
22087
  lines: whole.split("\n").filter((l) => l.length),
21776
22088
  cursor: encodeCursor({ dev: st.dev, ino: st.ino, off: off + Buffer.byteLength(whole) }),
@@ -21812,7 +22124,7 @@ function pauseDoc(state) {
21812
22124
  }
21813
22125
  function describeStream(r) {
21814
22126
  return {
21815
- run: basename13(r.streamPath).replace(/\.jsonl$/, ""),
22127
+ run: basename14(r.streamPath).replace(/\.jsonl$/, ""),
21816
22128
  pid: r.pid,
21817
22129
  ticket: r.ticket,
21818
22130
  role: r.role,
@@ -21849,7 +22161,7 @@ async function tailReply(path, q2, rotated, abort) {
21849
22161
  const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
21850
22162
  let r = readTail(path, { after, tail: tail3, rotated });
21851
22163
  while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
21852
- await waitForChange(dirname25(path), deadline - Date.now(), abort);
22164
+ await waitForChange(dirname26(path), deadline - Date.now(), abort);
21853
22165
  r = readTail(path, { after: r.cursor, rotated });
21854
22166
  }
21855
22167
  return reply2(200, r);
@@ -22039,21 +22351,21 @@ function crewJson(args, timeoutMs = 6e4) {
22039
22351
 
22040
22352
  // src/periscope-mcp.ts
22041
22353
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
22042
- import { basename as basename14, dirname as dirname26, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
22354
+ import { basename as basename15, dirname as dirname27, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
22043
22355
  import { z } from "zod";
22044
22356
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22045
22357
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
22046
22358
 
22047
22359
  // src/periscope-endpoints.ts
22048
22360
  import { randomBytes as randomBytes3 } from "crypto";
22049
- import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync30, readdirSync as readdirSync19, readFileSync as readFileSync39, rmSync as rmSync22, writeFileSync as writeFileSync32 } from "fs";
22361
+ import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync31, readdirSync as readdirSync19, readFileSync as readFileSync39, rmSync as rmSync22, writeFileSync as writeFileSync32 } from "fs";
22050
22362
  import { join as join47 } from "path";
22051
22363
  var secretDir = (stateDir) => join47(stateDir, "periscope-mcp");
22052
22364
  var secretFile = (stateDir, id) => join47(secretDir(stateDir), `${id}.json`);
22053
22365
  var endpointDir = (stateDir) => join47(stateDir, "periscope-endpoints");
22054
22366
  var endpointFile = (stateDir, id) => join47(endpointDir(stateDir), `${id}.json`);
22055
22367
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
22056
- mkdirSync30(secretDir(stateDir), { recursive: true, mode: 448 });
22368
+ mkdirSync31(secretDir(stateDir), { recursive: true, mode: 448 });
22057
22369
  const secret = randomBytes3(32).toString("base64url");
22058
22370
  const file = secretFile(stateDir, ticketRecordId);
22059
22371
  writeFileSync32(file, `${JSON.stringify({ secret })}
@@ -22080,7 +22392,7 @@ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
22080
22392
  }
22081
22393
  }
22082
22394
  function writePeriscopeEndpoint(stateDir, rec) {
22083
- mkdirSync30(endpointDir(stateDir), { recursive: true });
22395
+ mkdirSync31(endpointDir(stateDir), { recursive: true });
22084
22396
  writeFileSync32(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
22085
22397
  `);
22086
22398
  }
@@ -22177,12 +22489,12 @@ async function realAncestor(p) {
22177
22489
  }
22178
22490
  if (isLink) {
22179
22491
  if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
22180
- cur = resolve5(dirname26(cur), await readlink(cur));
22492
+ cur = resolve5(dirname27(cur), await readlink(cur));
22181
22493
  continue;
22182
22494
  }
22183
- const parent = dirname26(cur);
22495
+ const parent = dirname27(cur);
22184
22496
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
22185
- tail3.push(basename14(cur));
22497
+ tail3.push(basename15(cur));
22186
22498
  cur = parent;
22187
22499
  }
22188
22500
  }
@@ -22291,7 +22603,7 @@ function createPeriscopeMcpServer(d, sc) {
22291
22603
  if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
22292
22604
  const abs = await inTree(path);
22293
22605
  if (abs === await realpath(root)) return text("path must name a file", true);
22294
- await mkdir(dirname26(abs), { recursive: true });
22606
+ await mkdir(dirname27(abs), { recursive: true });
22295
22607
  await inTree(path);
22296
22608
  await writeFile(abs, content);
22297
22609
  return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
@@ -22331,9 +22643,9 @@ function createPeriscopeMcpServer(d, sc) {
22331
22643
  inputSchema: { path: z.string() },
22332
22644
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
22333
22645
  }, async ({ path }) => {
22334
- const parent = await inTree(dirname26(path) === "" ? "." : dirname26(path));
22335
- const abs = join48(parent, basename14(path));
22336
- if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
22646
+ const parent = await inTree(dirname27(path) === "" ? "." : dirname27(path));
22647
+ const abs = join48(parent, basename15(path));
22648
+ if (basename15(path) === ".git" || abs === parent) return text("path must name a file", true);
22337
22649
  const st = await lstat(abs).catch(() => void 0);
22338
22650
  if (!st) return text(`${path} does not exist`, true);
22339
22651
  if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
@@ -22453,7 +22765,7 @@ function findPeriscope(sidecars, segment) {
22453
22765
  }
22454
22766
 
22455
22767
  // src/cli.ts
22456
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname27(process.execPath) : resolve6(dirname27(fileURLToPath(import.meta.url)), "..");
22768
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname28(process.execPath) : resolve6(dirname28(fileURLToPath(import.meta.url)), "..");
22457
22769
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
22458
22770
  var CYCLE_SECONDS2 = 120;
22459
22771
  var STALE_CYCLES = 3;
@@ -22620,6 +22932,7 @@ function usage(problem, requested = false) {
22620
22932
  crew selftest [route] [--toolchain NAME] [--json] launch each toolchain as a run would and prove it reaches the tracker
22621
22933
  crew sample-ticket [route] [--remove] [--dry-run] [--json] file one ticket on a throwaway repo that walks the dev, QA and release loop
22622
22934
  crew ports [route] which checkout owns which ports, and what is up
22935
+ crew sandbox policy --route R --role ROLE [--repo NAME] [--json] print the sandbox policy a run of that role would get
22623
22936
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
22624
22937
  crew drop [route] NNN remove a merged ticket's worktree and branch
22625
22938
  crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
@@ -24878,7 +25191,7 @@ ${cu.tail.trimEnd()}
24878
25191
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
24879
25192
  const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync40(resolvedPath, "utf8")) : {};
24880
25193
  raw.agentSkills = result.agentSkills;
24881
- mkdirSync31(dirOf2(resolvedPath), { recursive: true });
25194
+ mkdirSync32(dirOf2(resolvedPath), { recursive: true });
24882
25195
  writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
24883
25196
  `);
24884
25197
  }
@@ -24956,7 +25269,7 @@ ${cu.tail.trimEnd()}
24956
25269
  const found = cfg.routes.find((r) => r.route === routeArg);
24957
25270
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
24958
25271
  const named2 = value("route") ? void 0 : positional[3];
24959
- const dir = named2 ? repoDirOf(configText, dirname27(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
25272
+ const dir = named2 ? repoDirOf(configText, dirname28(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
24960
25273
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
24961
25274
  const o = target ? found.repoOverrides[target.name] : void 0;
24962
25275
  runReposCheck(dir, {
@@ -24979,7 +25292,7 @@ ${cu.tail.trimEnd()}
24979
25292
  if (sub === "hooks") {
24980
25293
  try {
24981
25294
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
24982
- const dir = repoDirOf(configText, dirname27(cfg.configFile), a.route, a.name);
25295
+ const dir = repoDirOf(configText, dirname28(cfg.configFile), a.route, a.name);
24983
25296
  if (!existsSync45(dir)) throw new ReposError(`${dir} does not exist on disk`);
24984
25297
  const files = { exists: (r) => existsSync45(resolvePath(dir, r)), list: (r) => {
24985
25298
  try {
@@ -25146,7 +25459,7 @@ ${cu.tail.trimEnd()}
25146
25459
  }
25147
25460
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
25148
25461
  const text2 = configText;
25149
- const base = dirname27(cfg.configFile);
25462
+ const base = dirname28(cfg.configFile);
25150
25463
  const isCheckout = (d) => existsSync45(`${d}/.git`);
25151
25464
  try {
25152
25465
  if (sub === "list") {
@@ -25822,7 +26135,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25822
26135
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
25823
26136
  `);
25824
26137
  } else {
25825
- mkdirSync31(dirname27(resolvedPath), { recursive: true });
26138
+ mkdirSync32(dirname28(resolvedPath), { recursive: true });
25826
26139
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
25827
26140
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
25828
26141
  writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -25871,7 +26184,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25871
26184
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
25872
26185
  `);
25873
26186
  } else {
25874
- mkdirSync31(dirname27(apiKeyFile), { recursive: true });
26187
+ mkdirSync32(dirname28(apiKeyFile), { recursive: true });
25875
26188
  writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
25876
26189
  `, { mode: 384 });
25877
26190
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -26171,6 +26484,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26171
26484
  }
26172
26485
  break;
26173
26486
  }
26487
+ case "sandbox": {
26488
+ if (positional[1] !== "policy") {
26489
+ process.stderr.write("crew sandbox policy --route ROUTE --role ROLE [--repo NAME] [--json]\n");
26490
+ process.exit(2);
26491
+ }
26492
+ const only = value("route");
26493
+ const wanted = only ? cfg.routes.filter((r2) => r2.route === only) : cfg.routes;
26494
+ if (wanted.length !== 1) {
26495
+ process.stderr.write(`crew sandbox policy: ${only ? `no route "${only}"` : "name the route with --route"} (have: ${cfg.routes.map((r2) => r2.route).join(", ")})
26496
+ `);
26497
+ process.exit(2);
26498
+ }
26499
+ const role = value("role") ?? "dev";
26500
+ if (!ROLE_NAMES.includes(role)) {
26501
+ process.stderr.write(`crew sandbox policy: --role must be one of ${ROLE_NAMES.join(", ")}
26502
+ `);
26503
+ process.exit(2);
26504
+ }
26505
+ const r = wanted[0];
26506
+ const repoName = value("repo");
26507
+ const seat = resolveSeat(cfg.ship, r, role, {}, repoName);
26508
+ const input = sandboxPolicyInputFor(r, seat.toolchain, { harnessBin: seat.toolchain.bin, repo: repoName, sandbox: cfg.ship.sandbox });
26509
+ try {
26510
+ const policy = generateSandboxPolicy({ mirrorDir: cfg.ship.sandbox?.mirrorDir, workDir: cfg.ship.sandbox?.workDir, ...input });
26511
+ process.stdout.write(flag("json") ? `${JSON.stringify({ route: r.route, role, input, groups: policy.groups, yaml: policy.yaml }, null, 2)}
26512
+ ` : policy.yaml);
26513
+ } catch (e) {
26514
+ process.stderr.write(`crew sandbox policy: ${e.message}
26515
+ `);
26516
+ process.exit(1);
26517
+ }
26518
+ break;
26519
+ }
26174
26520
  case "reap": {
26175
26521
  emit.enter("worktree");
26176
26522
  let anything = false;
@@ -26768,7 +27114,7 @@ ${where.join("\n")}`);
26768
27114
  for (const f of repoP.config.worktrees.copy) {
26769
27115
  const src = join49(repoP.dir, f);
26770
27116
  if (!existsSync45(src)) continue;
26771
- mkdirSync31(dirOf2(join49(path, f)), { recursive: true });
27117
+ mkdirSync32(dirOf2(join49(path, f)), { recursive: true });
26772
27118
  copyFileSync2(src, join49(path, f));
26773
27119
  }
26774
27120
  const setup = repoP.config.hooks.setup;
@@ -27117,7 +27463,7 @@ ${where.join("\n")}`);
27117
27463
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
27118
27464
  break;
27119
27465
  }
27120
- mkdirSync31(cfg.ship.stateDir, { recursive: true });
27466
+ mkdirSync32(cfg.ship.stateDir, { recursive: true });
27121
27467
  writeFileSync33(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
27122
27468
  `);
27123
27469
  process.stdout.write(
@@ -27207,7 +27553,7 @@ ${where.join("\n")}`);
27207
27553
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
27208
27554
  const tracker = new Tracker(route2, cfg.ship);
27209
27555
  const configText = readFileSync40(cfg.configFile, "utf8");
27210
- const cfgBase = dirname27(cfg.configFile);
27556
+ const cfgBase = dirname28(cfg.configFile);
27211
27557
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
27212
27558
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
27213
27559
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
@@ -27251,7 +27597,7 @@ ${where.join("\n")}`);
27251
27597
  `);
27252
27598
  }
27253
27599
  }
27254
- if (existsSync45(join49(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
27600
+ if (existsSync45(join49(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
27255
27601
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
27256
27602
  break;
27257
27603
  }
@@ -27263,7 +27609,7 @@ ${where.join("\n")}`);
27263
27609
  break;
27264
27610
  }
27265
27611
  if (fresh) {
27266
- mkdirSync31(dir, { recursive: true });
27612
+ mkdirSync32(dir, { recursive: true });
27267
27613
  writeFileSync33(join49(dir, "README.md"), SAMPLE_README);
27268
27614
  writeFileSync33(join49(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
27269
27615
  const g = (...a) => {
@@ -27567,7 +27913,7 @@ ${where.join("\n")}`);
27567
27913
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
27568
27914
  // config-provenance display; this is the field that actually
27569
27915
  // describes where a worktree lands by default.
27570
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
27916
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename16(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
27571
27917
  worktreeParent: resolvePath(r.dir, "..")
27572
27918
  })),
27573
27919
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -27764,7 +28110,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
27764
28110
  }
27765
28111
  try {
27766
28112
  const evFile = eventFileFor(cfg.ship.stateDir);
27767
- const evSize = existsSync45(evFile) ? statSync14(evFile).size : 0;
28113
+ const evSize = existsSync45(evFile) ? statSync15(evFile).size : 0;
27768
28114
  process.stdout.write(
27769
28115
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
27770
28116
  ` : "events log: ok\n"
@@ -27798,7 +28144,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
27798
28144
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
27799
28145
  if (changed2) {
27800
28146
  process.stdout.write(
27801
- `daemon config: WARN ${changed2} changed at ${new Date(statSync14(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
28147
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync15(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
27802
28148
  `
27803
28149
  );
27804
28150
  }
@@ -27825,7 +28171,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
27825
28171
  );
27826
28172
  for (const r of await resolvedRepos(route2)) {
27827
28173
  const { config } = r;
27828
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
28174
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename16(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
27829
28175
  process.stdout.write(
27830
28176
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
27831
28177
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}