@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.
- package/dist/cli.js +500 -154
- 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
|
|
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
|
|
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 =
|
|
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 `${
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
|
5381
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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 (
|
|
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 ??
|
|
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" ? {
|
|
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
|
-
|
|
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),
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
12209
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
12316
|
-
import { dirname as
|
|
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
|
-
|
|
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(".") &&
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
12779
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
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:
|
|
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() ??
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
15009
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
15493
|
-
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as
|
|
15494
|
-
import { stringify as
|
|
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 =
|
|
15853
|
+
const nodeDir = dirname17(env.nodePath);
|
|
15542
15854
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
15543
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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
|
|
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([
|
|
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
|
|
15693
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
17899
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
18672
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
19436
|
-
const stem = `${
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
20227
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
|
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
|
|
20429
|
-
import { dirname as
|
|
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 =
|
|
20448
|
-
|
|
20449
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
22492
|
+
cur = resolve5(dirname27(cur), await readlink(cur));
|
|
22181
22493
|
continue;
|
|
22182
22494
|
}
|
|
22183
|
-
const parent =
|
|
22495
|
+
const parent = dirname27(cur);
|
|
22184
22496
|
if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
|
|
22185
|
-
tail3.push(
|
|
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(
|
|
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(
|
|
22335
|
-
const abs = join48(parent,
|
|
22336
|
-
if (
|
|
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) ?
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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")) &&
|
|
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
|
-
|
|
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>` : `${
|
|
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) ?
|
|
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(
|
|
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>` : `${
|
|
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}
|