@tablation/crew 0.108.0 → 0.109.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 +425 -131
  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);
@@ -11543,7 +11803,7 @@ function readSandboxState(stateDir) {
11543
11803
  }
11544
11804
  }
11545
11805
  function writeSandboxState(stateDir, state) {
11546
- mkdirSync13(stateDir, { recursive: true });
11806
+ mkdirSync14(stateDir, { recursive: true });
11547
11807
  const file = sandboxStatePath(stateDir);
11548
11808
  writeFileSync15(`${file}.tmp`, `${JSON.stringify(state, null, 2)}
11549
11809
  `);
@@ -11596,17 +11856,17 @@ async function buildSandboxImage(plan, deps) {
11596
11856
  return { kind: "reused", tag: plan.tag };
11597
11857
  }
11598
11858
  const logDir = sandboxLogDir(deps.stateDir);
11599
- mkdirSync13(logDir, { recursive: true });
11859
+ mkdirSync14(logDir, { recursive: true });
11600
11860
  const logPath = join26(logDir, `${slug(plan.repo)}-${now().toISOString().replace(/[:.]/g, "-")}.log`);
11601
11861
  const builtLayers = [];
11602
11862
  for (const layer of plan.layers) {
11603
11863
  if (deps.hasImage(layer.tag)) continue;
11604
11864
  const ctx = join26(tmpdir3(), `crew-sandbox-${hash([layer.tag, process.pid, Date.now()])}`);
11605
- mkdirSync13(ctx, { recursive: true });
11865
+ mkdirSync14(ctx, { recursive: true });
11606
11866
  try {
11607
11867
  writeFileSync15(join26(ctx, "Dockerfile"), layer.dockerfile);
11608
11868
  for (const [name, content] of Object.entries(layer.files)) {
11609
- mkdirSync13(dirname9(join26(ctx, name)), { recursive: true });
11869
+ mkdirSync14(dirname10(join26(ctx, name)), { recursive: true });
11610
11870
  writeFileSync15(join26(ctx, name), content);
11611
11871
  }
11612
11872
  const code = await deps.run(["build", "--tag", layer.tag, "--file", join26(ctx, "Dockerfile"), ctx], logPath);
@@ -11787,7 +12047,7 @@ function roleCapWarnings(ship) {
11787
12047
 
11788
12048
  // src/selftest.ts
11789
12049
  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";
12050
+ import { existsSync as existsSync25, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, readFileSync as readFileSync22, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
11791
12051
  import { tmpdir as tmpdir4 } from "os";
11792
12052
  import { join as join27 } from "path";
11793
12053
  var SELFTEST_TIMEOUT_MS = 12e4;
@@ -11995,7 +12255,7 @@ function readSelftest(stateDir) {
11995
12255
  function recordSelftest(stateDir, results) {
11996
12256
  const all = readSelftest(stateDir);
11997
12257
  for (const r of results) all[r.toolchain] = r;
11998
- mkdirSync14(stateDir, { recursive: true });
12258
+ mkdirSync15(stateDir, { recursive: true });
11999
12259
  writeFileSync16(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
12000
12260
  `);
12001
12261
  }
@@ -12008,7 +12268,7 @@ function doctorSelftestLines(stored, toolchains) {
12008
12268
  }
12009
12269
 
12010
12270
  // src/cli.ts
12011
- import { resolve as resolve6, dirname as dirname27, join as join49, basename as basename15 } from "path";
12271
+ import { resolve as resolve6, dirname as dirname28, join as join49, basename as basename16 } from "path";
12012
12272
  import { fileURLToPath } from "url";
12013
12273
  import { hostname as hostname4, homedir as homedir11 } from "os";
12014
12274
  import { spawnSync as spawnSync10 } from "child_process";
@@ -12205,8 +12465,8 @@ function rosterMarkdown(roster, meId) {
12205
12465
  }
12206
12466
 
12207
12467
  // 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";
12468
+ import { mkdirSync as mkdirSync16, readFileSync as readFileSync23, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
12469
+ import { dirname as dirname11, join as join28 } from "path";
12210
12470
  function lastSessionFile(stateDir) {
12211
12471
  return join28(stateDir, "connect-session.json");
12212
12472
  }
@@ -12226,7 +12486,7 @@ function readPointers(stateDir) {
12226
12486
  }
12227
12487
  function writePointers(stateDir, pointers) {
12228
12488
  const file = lastSessionFile(stateDir);
12229
- mkdirSync15(dirname10(file), { recursive: true });
12489
+ mkdirSync16(dirname11(file), { recursive: true });
12230
12490
  writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
12231
12491
  `, { mode: 384 });
12232
12492
  }
@@ -12312,8 +12572,8 @@ async function signOutSessions(o) {
12312
12572
  }
12313
12573
 
12314
12574
  // 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";
12575
+ import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
12576
+ import { dirname as dirname12, join as join29 } from "path";
12317
12577
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
12318
12578
  function clonedCheckoutsFile(stateDir) {
12319
12579
  return join29(stateDir, "cloned-checkouts.json");
@@ -12330,7 +12590,7 @@ function recordClonedCheckout(stateDir, dir) {
12330
12590
  const known = readClonedCheckouts(stateDir);
12331
12591
  if (known.includes(dir)) return;
12332
12592
  const file = clonedCheckoutsFile(stateDir);
12333
- mkdirSync16(dirname11(file), { recursive: true });
12593
+ mkdirSync17(dirname12(file), { recursive: true });
12334
12594
  writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
12335
12595
  `);
12336
12596
  }
@@ -12511,9 +12771,9 @@ async function runDeprovision(o, fx, deps) {
12511
12771
  }
12512
12772
 
12513
12773
  // src/pool-member.ts
12514
- import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
12774
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
12515
12775
  import { homedir as homedir8 } from "os";
12516
- import { dirname as dirname12, join as join30 } from "path";
12776
+ import { dirname as dirname13, join as join30 } from "path";
12517
12777
  var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
12518
12778
  function backoffMs(attempt) {
12519
12779
  return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
@@ -12715,7 +12975,7 @@ function fileMemberStore(dir) {
12715
12975
  }
12716
12976
  },
12717
12977
  write(f) {
12718
- mkdirSync17(dirname12(file), { recursive: true, mode: 448 });
12978
+ mkdirSync18(dirname13(file), { recursive: true, mode: 448 });
12719
12979
  const tmp = `${file}.tmp`;
12720
12980
  writeFileSync19(tmp, `${JSON.stringify(f)}
12721
12981
  `, { mode: 384 });
@@ -12775,8 +13035,8 @@ import { hostname as hostname3 } from "os";
12775
13035
  // src/machine-id.ts
12776
13036
  import { execFileSync as execFileSync8 } from "child_process";
12777
13037
  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";
13038
+ import { existsSync as existsSync27, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
13039
+ import { dirname as dirname14, join as join31 } from "path";
12780
13040
  function machineIdPathFor(stateDir) {
12781
13041
  return join31(stateDir, "machine-id");
12782
13042
  }
@@ -12810,7 +13070,7 @@ function ensureMachineId(stateDir, opts = {}) {
12810
13070
  const raw = (opts.read ?? readRawMachineId)();
12811
13071
  if (!raw) return void 0;
12812
13072
  const hashed = sha256(raw);
12813
- mkdirSync18(dirname13(path), { recursive: true });
13073
+ mkdirSync19(dirname14(path), { recursive: true });
12814
13074
  writeFileSync20(path, `${hashed}
12815
13075
  `, { mode: 384 });
12816
13076
  return hashed;
@@ -12833,8 +13093,8 @@ import { parseDocument as parseDocument2 } from "yaml";
12833
13093
 
12834
13094
  // src/repos-cmd.ts
12835
13095
  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";
13096
+ import { basename as basename10, join as join32, resolve as resolve3 } from "path";
13097
+ import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
12838
13098
  var ReposError = class extends Error {
12839
13099
  };
12840
13100
  function normalizeRemote(url) {
@@ -12905,7 +13165,7 @@ function configuredRepos(text2, route2, base) {
12905
13165
  });
12906
13166
  }
12907
13167
  const dir = m.get("dir");
12908
- return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
13168
+ return dir ? [{ name: basename10(String(dir)), dir: expandDir(String(dir), base) }] : [];
12909
13169
  }
12910
13170
  function cloneBaseOf(text2, route2, base, isCheckout) {
12911
13171
  const m = routeMapOf(parseDocument(text2), route2);
@@ -12994,7 +13254,7 @@ function planRepoAdd(i) {
12994
13254
  throw new ReposError(`${target} is already configured on route ${i.route}`);
12995
13255
  }
12996
13256
  const matched = matchRepoName(i.origin, i.resolved);
12997
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
13257
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename10(target);
12998
13258
  let warning;
12999
13259
  if (!matched) {
13000
13260
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -13047,7 +13307,7 @@ function planRepoRemove(text2, base, route2, path) {
13047
13307
  return { text: String(doc), name: hit.name };
13048
13308
  }
13049
13309
  function renderBlock(m) {
13050
- return stringify([m.toJSON()]).trimEnd();
13310
+ return stringify2([m.toJSON()]).trimEnd();
13051
13311
  }
13052
13312
  var LOCAL_RELEASE_MODES = /* @__PURE__ */ new Set(["integrate", "external"]);
13053
13313
  function planRepoRow(i) {
@@ -13454,7 +13714,7 @@ host=${host}
13454
13714
 
13455
13715
  // src/tunnel.ts
13456
13716
  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";
13717
+ import { existsSync as existsSync29, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
13458
13718
  import { randomInt } from "crypto";
13459
13719
  import { join as join33 } from "path";
13460
13720
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -13541,7 +13801,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
13541
13801
  }
13542
13802
  function writePersistedTunnel(stateDir, state) {
13543
13803
  const path = tunnelStatePath(stateDir, state.workspaceId);
13544
- mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
13804
+ mkdirSync20(join33(stateDir, "passengers"), { recursive: true });
13545
13805
  writeFileSync22(path, `${JSON.stringify(state, null, 2)}
13546
13806
  `);
13547
13807
  }
@@ -15005,8 +15265,8 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
15005
15265
  }
15006
15266
 
15007
15267
  // 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";
15268
+ import { existsSync as existsSync31, mkdirSync as mkdirSync21, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
15269
+ import { dirname as dirname16 } from "path";
15010
15270
  var REFRESH_COOLDOWN_MS = 2 * 6e4;
15011
15271
  var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
15012
15272
  var lastAsked = /* @__PURE__ */ new Map();
@@ -15038,7 +15298,7 @@ function persistRepoRows(stateDir, routeName, rows) {
15038
15298
  if (!existsSync31(path)) return;
15039
15299
  const raw = JSON.parse(readFileSync29(path, "utf8"));
15040
15300
  applyRepoRows(raw, rows);
15041
- mkdirSync20(dirname15(path), { recursive: true });
15301
+ mkdirSync21(dirname16(path), { recursive: true });
15042
15302
  writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
15043
15303
  `);
15044
15304
  }
@@ -15489,9 +15749,9 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
15489
15749
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
15490
15750
 
15491
15751
  // 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";
15752
+ import { delimiter, dirname as dirname17, join as join35 } from "path";
15753
+ import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync22, writeFileSync as writeFileSync25 } from "fs";
15754
+ import { stringify as stringify3 } from "yaml";
15495
15755
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
15496
15756
  function shouldRunWizard(opts) {
15497
15757
  if (!opts.isTTY) return false;
@@ -15538,9 +15798,9 @@ async function runWizard(p, env) {
15538
15798
  if (entered && isExec(entered)) agentBin = entered;
15539
15799
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
15540
15800
  }
15541
- const nodeDir = dirname16(env.nodePath);
15801
+ const nodeDir = dirname17(env.nodePath);
15542
15802
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
15543
- const baseDirs = [nodeDir, pnpmPath ? dirname16(pnpmPath) : void 0];
15803
+ const baseDirs = [nodeDir, pnpmPath ? dirname17(pnpmPath) : void 0];
15544
15804
  p.say(
15545
15805
  `
15546
15806
  Host Passengers lets this machine run the agent sessions that other people's
@@ -15551,14 +15811,14 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
15551
15811
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
15552
15812
  }
15553
15813
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
15554
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname16(dockerPath) : void 0]);
15814
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname17(dockerPath) : void 0]);
15555
15815
  if (extraPath) {
15556
15816
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
15557
15817
  }
15558
15818
  return { name, platform: env.platform, agentBin, extraPath, hostPassengers };
15559
15819
  }
15560
15820
  function renderShipBlock(a) {
15561
- return stringify2({
15821
+ return stringify3({
15562
15822
  ship: {
15563
15823
  name: a.name,
15564
15824
  platform: a.platform,
@@ -15573,7 +15833,7 @@ routes:
15573
15833
  ${routeBlock}`;
15574
15834
  }
15575
15835
  function writeNewConfig(path, text2) {
15576
- mkdirSync21(dirname16(path), { recursive: true });
15836
+ mkdirSync22(dirname17(path), { recursive: true });
15577
15837
  try {
15578
15838
  writeFileSync25(path, text2, { mode: 384, flag: "wx" });
15579
15839
  return true;
@@ -15609,7 +15869,7 @@ Wrote ${o.configPath}.
15609
15869
  // src/connect-json.ts
15610
15870
  import { accessSync as accessSync4, constants as constants4 } from "fs";
15611
15871
  import { homedir as homedir10 } from "os";
15612
- import { dirname as dirname17, join as join36 } from "path";
15872
+ import { dirname as dirname18, join as join36 } from "path";
15613
15873
  import { delimiter as delimiter2 } from "path";
15614
15874
  var CHOICE_EXIT_CODE = 3;
15615
15875
  function createEmitter(json2, write) {
@@ -15675,7 +15935,7 @@ function flagWizardAnswers(f, env) {
15675
15935
  const agentBin = f.agentBin ?? found?.bin;
15676
15936
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
15677
15937
  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]);
15938
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname18(env.nodePath), pnpmPath ? dirname18(pnpmPath) : void 0, dockerPath ? dirname18(dockerPath) : void 0]);
15679
15939
  return {
15680
15940
  answers: {
15681
15941
  name: f.shipName?.trim() || env.hostname,
@@ -15689,8 +15949,8 @@ function flagWizardAnswers(f, env) {
15689
15949
  }
15690
15950
 
15691
15951
  // 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";
15952
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync26, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync23, mkdtempSync as mkdtempSync3, rmSync as rmSync17 } from "fs";
15953
+ import { dirname as dirname19, join as join37 } from "path";
15694
15954
  import { tmpdir as tmpdir5 } from "os";
15695
15955
 
15696
15956
  // src/release.ts
@@ -16576,7 +16836,7 @@ function openReleaseCheckout(o) {
16576
16836
  for (const f of o.repo.worktrees.copy) {
16577
16837
  const src = join37(o.cwd, f);
16578
16838
  if (!existsSync33(src)) continue;
16579
- mkdirSync22(dirname18(join37(dir, f)), { recursive: true });
16839
+ mkdirSync23(dirname19(join37(dir, f)), { recursive: true });
16580
16840
  copyFileSync(src, join37(dir, f));
16581
16841
  }
16582
16842
  return { dir, branch, close };
@@ -17895,8 +18155,8 @@ function nextRoles(decision, ran) {
17895
18155
  }
17896
18156
 
17897
18157
  // 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";
18158
+ import { existsSync as existsSync35, mkdirSync as mkdirSync24, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
18159
+ import { dirname as dirname20 } from "path";
17900
18160
  import { parseDocument as parseDocument5 } from "yaml";
17901
18161
  var ShipNameError = class extends Error {
17902
18162
  };
@@ -17975,7 +18235,7 @@ function writeSyncedName(resolvedFile, name) {
17975
18235
  }
17976
18236
  if (raw.shipName === name) return;
17977
18237
  raw.shipName = name;
17978
- mkdirSync23(dirname19(resolvedFile), { recursive: true });
18238
+ mkdirSync24(dirname20(resolvedFile), { recursive: true });
17979
18239
  const tmp = `${resolvedFile}.tmp${process.pid}`;
17980
18240
  writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
17981
18241
  `);
@@ -18668,8 +18928,8 @@ function describeCapability(i) {
18668
18928
  }
18669
18929
 
18670
18930
  // 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";
18931
+ import { existsSync as existsSync36, mkdirSync as mkdirSync25, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
18932
+ import { dirname as dirname21 } from "path";
18673
18933
  async function runAgentsSync(target, o) {
18674
18934
  const dryRun = o.dryRun ?? false;
18675
18935
  const force = o.force ?? false;
@@ -18706,7 +18966,7 @@ async function runAgentsSync(target, o) {
18706
18966
  raw.agentPersonas = personaResult.agentPersonas;
18707
18967
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
18708
18968
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
18709
- mkdirSync24(dirname20(resolvedPath), { recursive: true });
18969
+ mkdirSync25(dirname21(resolvedPath), { recursive: true });
18710
18970
  writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
18711
18971
  `);
18712
18972
  if (target.resolved) {
@@ -18812,11 +19072,11 @@ async function showLogEntry(route2, entryId, opts = {}) {
18812
19072
  // src/repos-check.ts
18813
19073
  import { spawnSync as spawnSync9 } from "child_process";
18814
19074
  import { readFileSync as readFileSync33 } from "fs";
18815
- import { basename as basename10 } from "path";
19075
+ import { basename as basename11 } from "path";
18816
19076
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
18817
19077
  function checkHookSyntax(script, shell) {
18818
19078
  const spec = resolveShell(shell);
18819
- const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
19079
+ const name = basename11(spec.bin).replace(/\.exe$/i, "").toLowerCase();
18820
19080
  let run3;
18821
19081
  if (POSIX_SHELL.test(name)) {
18822
19082
  run3 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
@@ -18835,7 +19095,7 @@ function checkHookSyntax(script, shell) {
18835
19095
  return { status: "error", message };
18836
19096
  }
18837
19097
  function renderSamples(cfg, dir) {
18838
- const checkout = basename10(dir.replace(/[/\\]+$/, ""));
19098
+ const checkout = basename11(dir.replace(/[/\\]+$/, ""));
18839
19099
  const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
18840
19100
  const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
18841
19101
  return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
@@ -19299,7 +19559,7 @@ ${run3.err}` });
19299
19559
  }
19300
19560
 
19301
19561
  // src/sample-ticket.ts
19302
- import { basename as basename11, dirname as dirname21, join as join38 } from "path";
19562
+ import { basename as basename12, dirname as dirname22, join as join38 } from "path";
19303
19563
  var SAMPLE_REPO = "crew-sample";
19304
19564
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
19305
19565
  var SAMPLE_MARKER = "sample: true";
@@ -19322,7 +19582,7 @@ has something safe to work on: its test and build hooks always pass, it has no
19322
19582
  remote, and \`crew sample-ticket --remove\` deletes it.
19323
19583
  `;
19324
19584
  function sampleDir(existing, reposBase, workspace) {
19325
- if (existing.length > 0) return join38(dirname21(existing[0]), SAMPLE_REPO);
19585
+ if (existing.length > 0) return join38(dirname22(existing[0]), SAMPLE_REPO);
19326
19586
  return join38(reposBase, workspace, SAMPLE_REPO);
19327
19587
  }
19328
19588
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
@@ -19368,7 +19628,7 @@ function planSampleTicket(i) {
19368
19628
 
19369
19629
  // src/worktree-sweep.ts
19370
19630
  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";
19631
+ import { basename as basename13, dirname as dirname23, join as join39, resolve as resolve4 } from "path";
19372
19632
  function planWorktreeSweep(target, tickets, contract) {
19373
19633
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
19374
19634
  const verified = contract.statuses.verified;
@@ -19409,7 +19669,7 @@ function ticketForWorktree(w, byNumber, contract) {
19409
19669
  const t = num ? byNumber.get(num) : void 0;
19410
19670
  if (t && ticketStems(t).has(stem)) return t;
19411
19671
  }
19412
- const dir = basename12(w.path);
19672
+ const dir = basename13(w.path);
19413
19673
  const m = DIR_NUMBER.exec(dir);
19414
19674
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
19415
19675
  if (!w.branch) {
@@ -19425,15 +19685,15 @@ function worktreeTicketNumbers(target) {
19425
19685
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
19426
19686
  if (n) nums.add(n);
19427
19687
  }
19428
- const m = DIR_NUMBER_SUFFIXED.exec(basename12(w.path));
19688
+ const m = DIR_NUMBER_SUFFIXED.exec(basename13(w.path));
19429
19689
  if (m) nums.add(m[1]);
19430
19690
  }
19431
19691
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
19432
19692
  return [...nums];
19433
19693
  }
19434
19694
  function siblingWorktreeDirs(target) {
19435
- const parent = dirname22(target.dir.replace(/[/\\]+$/, ""));
19436
- const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
19695
+ const parent = dirname23(target.dir.replace(/[/\\]+$/, ""));
19696
+ const stem = `${basename13(target.dir.replace(/[/\\]+$/, ""))}-`;
19437
19697
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
19438
19698
  let names;
19439
19699
  try {
@@ -19458,7 +19718,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
19458
19718
  }
19459
19719
  function warnOrphanedWorktreeDirs(paths, log, held) {
19460
19720
  for (const p of paths) {
19461
- const number = DIR_NUMBER_SUFFIXED.exec(basename12(p))?.[1];
19721
+ const number = DIR_NUMBER_SUFFIXED.exec(basename13(p))?.[1];
19462
19722
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
19463
19723
  if (when) {
19464
19724
  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 +19862,7 @@ function isTicketBranch(name, base, stems) {
19602
19862
  }
19603
19863
  function sweepCandidatesExist(cwd, base) {
19604
19864
  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;
19865
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename13(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
19606
19866
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
19607
19867
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
19608
19868
  }
@@ -19698,11 +19958,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
19698
19958
  }
19699
19959
 
19700
19960
  // 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";
19961
+ 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 statSync14, readdirSync as readdirSync20 } from "fs";
19702
19962
  import { createInterface } from "readline/promises";
19703
19963
 
19704
19964
  // src/periscope-cmd.ts
19705
- import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
19965
+ import { existsSync as existsSync39, mkdirSync as mkdirSync26, readFileSync as readFileSync35, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
19706
19966
  import { join as join41 } from "path";
19707
19967
  var PeriscopeError = class extends Error {
19708
19968
  };
@@ -19720,7 +19980,7 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
19720
19980
  }
19721
19981
  function writePeriscopePrior(stateDir, ticketRecordId, prior) {
19722
19982
  try {
19723
- mkdirSync25(priorDir(stateDir), { recursive: true });
19983
+ mkdirSync26(priorDir(stateDir), { recursive: true });
19724
19984
  writeFileSync29(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
19725
19985
  } catch {
19726
19986
  }
@@ -20223,8 +20483,8 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
20223
20483
 
20224
20484
  // src/ssh-keys.ts
20225
20485
  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";
20486
+ import { existsSync as existsSync40, mkdirSync as mkdirSync27, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
20487
+ import { dirname as dirname24, join as join43 } from "path";
20228
20488
  var SshKeyError = class extends Error {
20229
20489
  };
20230
20490
  function shipSshKeyPathFor(stateDir) {
@@ -20234,7 +20494,7 @@ function ensureShipSshKeypair(stateDir) {
20234
20494
  const privateKeyPath = shipSshKeyPathFor(stateDir);
20235
20495
  const publicKeyPath = `${privateKeyPath}.pub`;
20236
20496
  if (!existsSync40(privateKeyPath)) {
20237
- mkdirSync26(dirname23(privateKeyPath), { recursive: true });
20497
+ mkdirSync27(dirname24(privateKeyPath), { recursive: true });
20238
20498
  try {
20239
20499
  execFileSync11("ssh-keygen", [
20240
20500
  "-t",
@@ -20421,12 +20681,12 @@ async function unregisterEndpoint(workspaceId, info, d) {
20421
20681
  import { createServer as createHttpServer } from "http";
20422
20682
  import { spawn as spawn8 } from "child_process";
20423
20683
  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";
20684
+ import { existsSync as existsSync41, mkdirSync as mkdirSync29, readFileSync as readFileSync37, writeFileSync as writeFileSync30, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
20425
20685
  import { join as join44 } from "path";
20426
20686
 
20427
20687
  // src/passenger-mirror.ts
20428
- import { mkdirSync as mkdirSync27 } from "fs";
20429
- import { dirname as dirname24 } from "path";
20688
+ import { mkdirSync as mkdirSync28 } from "fs";
20689
+ import { dirname as dirname25 } from "path";
20430
20690
  function mirrorRepoOf(spec) {
20431
20691
  const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
20432
20692
  if (fromOrigin) return fromOrigin;
@@ -20444,9 +20704,9 @@ async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCa
20444
20704
  continue;
20445
20705
  }
20446
20706
  try {
20447
- const parent = dirname24(spec.dir);
20448
- mkdirSync27(parent, { recursive: true });
20449
- mkdirSync27(spec.dir, { recursive: true });
20707
+ const parent = dirname25(spec.dir);
20708
+ mkdirSync28(parent, { recursive: true });
20709
+ mkdirSync28(spec.dir, { recursive: true });
20450
20710
  const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
20451
20711
  out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
20452
20712
  } catch (e) {
@@ -20565,7 +20825,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
20565
20825
  }
20566
20826
  function writePersistedSyncDaemon(stateDir, state) {
20567
20827
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
20568
- mkdirSync28(join44(stateDir, "passengers"), { recursive: true });
20828
+ mkdirSync29(join44(stateDir, "passengers"), { recursive: true });
20569
20829
  writeFileSync30(path, `${JSON.stringify(state, null, 2)}
20570
20830
  `);
20571
20831
  }
@@ -20675,7 +20935,7 @@ function describeCause(err3) {
20675
20935
  }
20676
20936
 
20677
20937
  // 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";
20938
+ import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync12, writeFileSync as writeFileSync31 } from "fs";
20679
20939
  import { join as join45 } from "path";
20680
20940
  import { randomBytes as randomBytes2 } from "crypto";
20681
20941
  var DEFAULT_FLOOR_MS = 5e3;
@@ -20789,7 +21049,7 @@ function daemonStampFor(stateDir) {
20789
21049
  return join45(stateDir, "daemon.started");
20790
21050
  }
20791
21051
  function noteDaemonStarted(stateDir, now = Date.now()) {
20792
- mkdirSync29(stateDir, { recursive: true });
21052
+ mkdirSync30(stateDir, { recursive: true });
20793
21053
  rmSync20(reloadSentinelFor(stateDir), { force: true });
20794
21054
  writeFileSync31(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
20795
21055
  `);
@@ -21373,7 +21633,7 @@ import {
21373
21633
  } from "fs";
21374
21634
  import { createConnection } from "net";
21375
21635
  import { createServer } from "http";
21376
- import { basename as basename13, dirname as dirname25, join as join46 } from "path";
21636
+ import { basename as basename14, dirname as dirname26, join as join46 } from "path";
21377
21637
  import { execFile as execFile3, spawn as spawn10 } from "child_process";
21378
21638
 
21379
21639
  // src/periscope-control.ts
@@ -21689,7 +21949,7 @@ function candidates(live, rotated) {
21689
21949
  const out = [live];
21690
21950
  if (!rotated) return out;
21691
21951
  try {
21692
- for (const n of readdirSync18(dirname25(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname25(live), n));
21952
+ for (const n of readdirSync18(dirname26(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname26(live), n));
21693
21953
  } catch {
21694
21954
  }
21695
21955
  return out;
@@ -21812,7 +22072,7 @@ function pauseDoc(state) {
21812
22072
  }
21813
22073
  function describeStream(r) {
21814
22074
  return {
21815
- run: basename13(r.streamPath).replace(/\.jsonl$/, ""),
22075
+ run: basename14(r.streamPath).replace(/\.jsonl$/, ""),
21816
22076
  pid: r.pid,
21817
22077
  ticket: r.ticket,
21818
22078
  role: r.role,
@@ -21849,7 +22109,7 @@ async function tailReply(path, q2, rotated, abort) {
21849
22109
  const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
21850
22110
  let r = readTail(path, { after, tail: tail3, rotated });
21851
22111
  while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
21852
- await waitForChange(dirname25(path), deadline - Date.now(), abort);
22112
+ await waitForChange(dirname26(path), deadline - Date.now(), abort);
21853
22113
  r = readTail(path, { after: r.cursor, rotated });
21854
22114
  }
21855
22115
  return reply2(200, r);
@@ -22039,21 +22299,21 @@ function crewJson(args, timeoutMs = 6e4) {
22039
22299
 
22040
22300
  // src/periscope-mcp.ts
22041
22301
  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";
22302
+ import { basename as basename15, dirname as dirname27, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
22043
22303
  import { z } from "zod";
22044
22304
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22045
22305
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
22046
22306
 
22047
22307
  // src/periscope-endpoints.ts
22048
22308
  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";
22309
+ 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
22310
  import { join as join47 } from "path";
22051
22311
  var secretDir = (stateDir) => join47(stateDir, "periscope-mcp");
22052
22312
  var secretFile = (stateDir, id) => join47(secretDir(stateDir), `${id}.json`);
22053
22313
  var endpointDir = (stateDir) => join47(stateDir, "periscope-endpoints");
22054
22314
  var endpointFile = (stateDir, id) => join47(endpointDir(stateDir), `${id}.json`);
22055
22315
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
22056
- mkdirSync30(secretDir(stateDir), { recursive: true, mode: 448 });
22316
+ mkdirSync31(secretDir(stateDir), { recursive: true, mode: 448 });
22057
22317
  const secret = randomBytes3(32).toString("base64url");
22058
22318
  const file = secretFile(stateDir, ticketRecordId);
22059
22319
  writeFileSync32(file, `${JSON.stringify({ secret })}
@@ -22080,7 +22340,7 @@ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
22080
22340
  }
22081
22341
  }
22082
22342
  function writePeriscopeEndpoint(stateDir, rec) {
22083
- mkdirSync30(endpointDir(stateDir), { recursive: true });
22343
+ mkdirSync31(endpointDir(stateDir), { recursive: true });
22084
22344
  writeFileSync32(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
22085
22345
  `);
22086
22346
  }
@@ -22177,12 +22437,12 @@ async function realAncestor(p) {
22177
22437
  }
22178
22438
  if (isLink) {
22179
22439
  if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
22180
- cur = resolve5(dirname26(cur), await readlink(cur));
22440
+ cur = resolve5(dirname27(cur), await readlink(cur));
22181
22441
  continue;
22182
22442
  }
22183
- const parent = dirname26(cur);
22443
+ const parent = dirname27(cur);
22184
22444
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
22185
- tail3.push(basename14(cur));
22445
+ tail3.push(basename15(cur));
22186
22446
  cur = parent;
22187
22447
  }
22188
22448
  }
@@ -22291,7 +22551,7 @@ function createPeriscopeMcpServer(d, sc) {
22291
22551
  if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
22292
22552
  const abs = await inTree(path);
22293
22553
  if (abs === await realpath(root)) return text("path must name a file", true);
22294
- await mkdir(dirname26(abs), { recursive: true });
22554
+ await mkdir(dirname27(abs), { recursive: true });
22295
22555
  await inTree(path);
22296
22556
  await writeFile(abs, content);
22297
22557
  return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
@@ -22331,9 +22591,9 @@ function createPeriscopeMcpServer(d, sc) {
22331
22591
  inputSchema: { path: z.string() },
22332
22592
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
22333
22593
  }, 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);
22594
+ const parent = await inTree(dirname27(path) === "" ? "." : dirname27(path));
22595
+ const abs = join48(parent, basename15(path));
22596
+ if (basename15(path) === ".git" || abs === parent) return text("path must name a file", true);
22337
22597
  const st = await lstat(abs).catch(() => void 0);
22338
22598
  if (!st) return text(`${path} does not exist`, true);
22339
22599
  if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
@@ -22453,7 +22713,7 @@ function findPeriscope(sidecars, segment) {
22453
22713
  }
22454
22714
 
22455
22715
  // src/cli.ts
22456
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname27(process.execPath) : resolve6(dirname27(fileURLToPath(import.meta.url)), "..");
22716
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname28(process.execPath) : resolve6(dirname28(fileURLToPath(import.meta.url)), "..");
22457
22717
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
22458
22718
  var CYCLE_SECONDS2 = 120;
22459
22719
  var STALE_CYCLES = 3;
@@ -22620,6 +22880,7 @@ function usage(problem, requested = false) {
22620
22880
  crew selftest [route] [--toolchain NAME] [--json] launch each toolchain as a run would and prove it reaches the tracker
22621
22881
  crew sample-ticket [route] [--remove] [--dry-run] [--json] file one ticket on a throwaway repo that walks the dev, QA and release loop
22622
22882
  crew ports [route] which checkout owns which ports, and what is up
22883
+ crew sandbox policy --route R --role ROLE [--repo NAME] [--json] print the sandbox policy a run of that role would get
22623
22884
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
22624
22885
  crew drop [route] NNN remove a merged ticket's worktree and branch
22625
22886
  crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
@@ -24878,7 +25139,7 @@ ${cu.tail.trimEnd()}
24878
25139
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
24879
25140
  const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync40(resolvedPath, "utf8")) : {};
24880
25141
  raw.agentSkills = result.agentSkills;
24881
- mkdirSync31(dirOf2(resolvedPath), { recursive: true });
25142
+ mkdirSync32(dirOf2(resolvedPath), { recursive: true });
24882
25143
  writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
24883
25144
  `);
24884
25145
  }
@@ -24956,7 +25217,7 @@ ${cu.tail.trimEnd()}
24956
25217
  const found = cfg.routes.find((r) => r.route === routeArg);
24957
25218
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
24958
25219
  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] ?? ".");
25220
+ const dir = named2 ? repoDirOf(configText, dirname28(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
24960
25221
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
24961
25222
  const o = target ? found.repoOverrides[target.name] : void 0;
24962
25223
  runReposCheck(dir, {
@@ -24979,7 +25240,7 @@ ${cu.tail.trimEnd()}
24979
25240
  if (sub === "hooks") {
24980
25241
  try {
24981
25242
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
24982
- const dir = repoDirOf(configText, dirname27(cfg.configFile), a.route, a.name);
25243
+ const dir = repoDirOf(configText, dirname28(cfg.configFile), a.route, a.name);
24983
25244
  if (!existsSync45(dir)) throw new ReposError(`${dir} does not exist on disk`);
24984
25245
  const files = { exists: (r) => existsSync45(resolvePath(dir, r)), list: (r) => {
24985
25246
  try {
@@ -25146,7 +25407,7 @@ ${cu.tail.trimEnd()}
25146
25407
  }
25147
25408
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
25148
25409
  const text2 = configText;
25149
- const base = dirname27(cfg.configFile);
25410
+ const base = dirname28(cfg.configFile);
25150
25411
  const isCheckout = (d) => existsSync45(`${d}/.git`);
25151
25412
  try {
25152
25413
  if (sub === "list") {
@@ -25822,7 +26083,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25822
26083
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
25823
26084
  `);
25824
26085
  } else {
25825
- mkdirSync31(dirname27(resolvedPath), { recursive: true });
26086
+ mkdirSync32(dirname28(resolvedPath), { recursive: true });
25826
26087
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
25827
26088
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
25828
26089
  writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -25871,7 +26132,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
25871
26132
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
25872
26133
  `);
25873
26134
  } else {
25874
- mkdirSync31(dirname27(apiKeyFile), { recursive: true });
26135
+ mkdirSync32(dirname28(apiKeyFile), { recursive: true });
25875
26136
  writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
25876
26137
  `, { mode: 384 });
25877
26138
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -26171,6 +26432,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26171
26432
  }
26172
26433
  break;
26173
26434
  }
26435
+ case "sandbox": {
26436
+ if (positional[1] !== "policy") {
26437
+ process.stderr.write("crew sandbox policy --route ROUTE --role ROLE [--repo NAME] [--json]\n");
26438
+ process.exit(2);
26439
+ }
26440
+ const only = value("route");
26441
+ const wanted = only ? cfg.routes.filter((r2) => r2.route === only) : cfg.routes;
26442
+ if (wanted.length !== 1) {
26443
+ process.stderr.write(`crew sandbox policy: ${only ? `no route "${only}"` : "name the route with --route"} (have: ${cfg.routes.map((r2) => r2.route).join(", ")})
26444
+ `);
26445
+ process.exit(2);
26446
+ }
26447
+ const role = value("role") ?? "dev";
26448
+ if (!ROLE_NAMES.includes(role)) {
26449
+ process.stderr.write(`crew sandbox policy: --role must be one of ${ROLE_NAMES.join(", ")}
26450
+ `);
26451
+ process.exit(2);
26452
+ }
26453
+ const r = wanted[0];
26454
+ const repoName = value("repo");
26455
+ const seat = resolveSeat(cfg.ship, r, role, {}, repoName);
26456
+ const input = sandboxPolicyInputFor(r, seat.toolchain, { harnessBin: seat.toolchain.bin, repo: repoName, sandbox: cfg.ship.sandbox });
26457
+ try {
26458
+ const policy = generateSandboxPolicy({ mirrorDir: cfg.ship.sandbox?.mirrorDir, workDir: cfg.ship.sandbox?.workDir, ...input });
26459
+ process.stdout.write(flag("json") ? `${JSON.stringify({ route: r.route, role, input, groups: policy.groups, yaml: policy.yaml }, null, 2)}
26460
+ ` : policy.yaml);
26461
+ } catch (e) {
26462
+ process.stderr.write(`crew sandbox policy: ${e.message}
26463
+ `);
26464
+ process.exit(1);
26465
+ }
26466
+ break;
26467
+ }
26174
26468
  case "reap": {
26175
26469
  emit.enter("worktree");
26176
26470
  let anything = false;
@@ -26768,7 +27062,7 @@ ${where.join("\n")}`);
26768
27062
  for (const f of repoP.config.worktrees.copy) {
26769
27063
  const src = join49(repoP.dir, f);
26770
27064
  if (!existsSync45(src)) continue;
26771
- mkdirSync31(dirOf2(join49(path, f)), { recursive: true });
27065
+ mkdirSync32(dirOf2(join49(path, f)), { recursive: true });
26772
27066
  copyFileSync2(src, join49(path, f));
26773
27067
  }
26774
27068
  const setup = repoP.config.hooks.setup;
@@ -27117,7 +27411,7 @@ ${where.join("\n")}`);
27117
27411
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
27118
27412
  break;
27119
27413
  }
27120
- mkdirSync31(cfg.ship.stateDir, { recursive: true });
27414
+ mkdirSync32(cfg.ship.stateDir, { recursive: true });
27121
27415
  writeFileSync33(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
27122
27416
  `);
27123
27417
  process.stdout.write(
@@ -27207,7 +27501,7 @@ ${where.join("\n")}`);
27207
27501
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
27208
27502
  const tracker = new Tracker(route2, cfg.ship);
27209
27503
  const configText = readFileSync40(cfg.configFile, "utf8");
27210
- const cfgBase = dirname27(cfg.configFile);
27504
+ const cfgBase = dirname28(cfg.configFile);
27211
27505
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
27212
27506
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
27213
27507
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
@@ -27251,7 +27545,7 @@ ${where.join("\n")}`);
27251
27545
  `);
27252
27546
  }
27253
27547
  }
27254
- if (existsSync45(join49(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
27548
+ if (existsSync45(join49(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
27255
27549
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
27256
27550
  break;
27257
27551
  }
@@ -27263,7 +27557,7 @@ ${where.join("\n")}`);
27263
27557
  break;
27264
27558
  }
27265
27559
  if (fresh) {
27266
- mkdirSync31(dir, { recursive: true });
27560
+ mkdirSync32(dir, { recursive: true });
27267
27561
  writeFileSync33(join49(dir, "README.md"), SAMPLE_README);
27268
27562
  writeFileSync33(join49(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
27269
27563
  const g = (...a) => {
@@ -27567,7 +27861,7 @@ ${where.join("\n")}`);
27567
27861
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
27568
27862
  // config-provenance display; this is the field that actually
27569
27863
  // 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>`),
27864
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename16(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
27571
27865
  worktreeParent: resolvePath(r.dir, "..")
27572
27866
  })),
27573
27867
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -27825,7 +28119,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
27825
28119
  );
27826
28120
  for (const r of await resolvedRepos(route2)) {
27827
28121
  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)}`);
28122
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename16(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
27829
28123
  process.stdout.write(
27830
28124
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
27831
28125
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}