@tablation/crew 0.103.0 → 0.105.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 +1491 -895
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1912,8 +1912,8 @@ function configureSecretStore(c) {
|
|
|
1912
1912
|
function keysDir(stateDir) {
|
|
1913
1913
|
return join10(stateDir, "keys");
|
|
1914
1914
|
}
|
|
1915
|
-
function entryName(host,
|
|
1916
|
-
return `${createHash2("sha256").update(`${host}\0${
|
|
1915
|
+
function entryName(host, slug2) {
|
|
1916
|
+
return `${createHash2("sha256").update(`${host}\0${slug2}`).digest("hex")}.json`;
|
|
1917
1917
|
}
|
|
1918
1918
|
var FileSessionStore = class {
|
|
1919
1919
|
dir;
|
|
@@ -1924,23 +1924,23 @@ var FileSessionStore = class {
|
|
|
1924
1924
|
mkdirSync6(this.dir, { recursive: true, mode: 448 });
|
|
1925
1925
|
if ((statSync3(this.dir).mode & 63) !== 0) chmodSync2(this.dir, 448);
|
|
1926
1926
|
}
|
|
1927
|
-
async get(host,
|
|
1928
|
-
const file = join10(this.dir, entryName(host,
|
|
1927
|
+
async get(host, slug2) {
|
|
1928
|
+
const file = join10(this.dir, entryName(host, slug2));
|
|
1929
1929
|
if (!existsSync8(this.dir)) return void 0;
|
|
1930
1930
|
accessSync(this.dir, constants.R_OK | constants.X_OK);
|
|
1931
1931
|
if (!existsSync8(file)) return void 0;
|
|
1932
1932
|
return JSON.parse(readFileSync9(file, "utf8"));
|
|
1933
1933
|
}
|
|
1934
|
-
async set(host,
|
|
1934
|
+
async set(host, slug2, session) {
|
|
1935
1935
|
this.ensureDir();
|
|
1936
|
-
const file = join10(this.dir, entryName(host,
|
|
1936
|
+
const file = join10(this.dir, entryName(host, slug2));
|
|
1937
1937
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
1938
1938
|
writeFileSync7(tmp, JSON.stringify(session), { mode: 384 });
|
|
1939
1939
|
renameSync2(tmp, file);
|
|
1940
1940
|
}
|
|
1941
|
-
async clear(host,
|
|
1941
|
+
async clear(host, slug2) {
|
|
1942
1942
|
if (!existsSync8(this.dir)) return;
|
|
1943
|
-
rmSync7(join10(this.dir, entryName(host,
|
|
1943
|
+
rmSync7(join10(this.dir, entryName(host, slug2)), { force: true });
|
|
1944
1944
|
}
|
|
1945
1945
|
/** Whether a key could be written here right now, without leaving anything behind. */
|
|
1946
1946
|
writable() {
|
|
@@ -1973,23 +1973,23 @@ var CrewHintedStore = class {
|
|
|
1973
1973
|
constructor(inner) {
|
|
1974
1974
|
this.inner = inner;
|
|
1975
1975
|
}
|
|
1976
|
-
async get(host,
|
|
1976
|
+
async get(host, slug2) {
|
|
1977
1977
|
try {
|
|
1978
|
-
return await this.inner.get(host,
|
|
1978
|
+
return await this.inner.get(host, slug2);
|
|
1979
1979
|
} catch (e) {
|
|
1980
1980
|
throw crewifyStoreError(e);
|
|
1981
1981
|
}
|
|
1982
1982
|
}
|
|
1983
|
-
async set(host,
|
|
1983
|
+
async set(host, slug2, s) {
|
|
1984
1984
|
try {
|
|
1985
|
-
await this.inner.set(host,
|
|
1985
|
+
await this.inner.set(host, slug2, s);
|
|
1986
1986
|
} catch (e) {
|
|
1987
1987
|
throw crewifyStoreError(e);
|
|
1988
1988
|
}
|
|
1989
1989
|
}
|
|
1990
|
-
async clear(host,
|
|
1990
|
+
async clear(host, slug2) {
|
|
1991
1991
|
try {
|
|
1992
|
-
await this.inner.clear(host,
|
|
1992
|
+
await this.inner.clear(host, slug2);
|
|
1993
1993
|
} catch (e) {
|
|
1994
1994
|
throw crewifyStoreError(e);
|
|
1995
1995
|
}
|
|
@@ -2102,7 +2102,8 @@ var TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
|
2102
2102
|
"docs",
|
|
2103
2103
|
"hooks",
|
|
2104
2104
|
"labels",
|
|
2105
|
-
"release"
|
|
2105
|
+
"release",
|
|
2106
|
+
"sandbox"
|
|
2106
2107
|
]);
|
|
2107
2108
|
var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote", "lowercase"]);
|
|
2108
2109
|
var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag|issue)\}/g;
|
|
@@ -2169,6 +2170,36 @@ function rejectUnknown(obj, allowed, where, file) {
|
|
|
2169
2170
|
);
|
|
2170
2171
|
}
|
|
2171
2172
|
}
|
|
2173
|
+
var PACKAGE_NAME = /^[a-z0-9][a-z0-9+.\-]*(=[A-Za-z0-9.+:~\-]+)?$/;
|
|
2174
|
+
function stringList(raw, where, file) {
|
|
2175
|
+
if (raw === void 0 || raw === null) return [];
|
|
2176
|
+
if (!Array.isArray(raw)) throw new RepoConfigError(`${file}: ${where} must be a list of strings`);
|
|
2177
|
+
return raw.map((v) => {
|
|
2178
|
+
if (typeof v !== "string" || v.trim() === "") throw new RepoConfigError(`${file}: ${where} entries must be non-empty strings`);
|
|
2179
|
+
return v;
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
function parseSandbox(raw, file) {
|
|
2183
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
2184
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new RepoConfigError(`${file}: sandbox must be a mapping`);
|
|
2185
|
+
const r = raw;
|
|
2186
|
+
const packages = stringList(r.packages, "sandbox.packages", file);
|
|
2187
|
+
const setup = stringList(r.setup, "sandbox.setup", file);
|
|
2188
|
+
for (const p of packages) {
|
|
2189
|
+
if (!PACKAGE_NAME.test(p)) throw new RepoConfigError(`${file}: sandbox.packages entry "${p}" is not a package name (lowercase letters, digits, + . - and an optional =version)`);
|
|
2190
|
+
}
|
|
2191
|
+
let image;
|
|
2192
|
+
if (r.image !== void 0 && r.image !== null) {
|
|
2193
|
+
if (typeof r.image !== "string" || r.image.trim() === "" || /\s/.test(r.image)) {
|
|
2194
|
+
throw new RepoConfigError(`${file}: sandbox.image must be an image reference with no whitespace`);
|
|
2195
|
+
}
|
|
2196
|
+
image = r.image;
|
|
2197
|
+
if (packages.length > 0) {
|
|
2198
|
+
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`);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return { ...image ? { image } : {}, packages, setup };
|
|
2202
|
+
}
|
|
2172
2203
|
function parseRepoConfig(text2, file) {
|
|
2173
2204
|
let raw;
|
|
2174
2205
|
try {
|
|
@@ -2187,6 +2218,7 @@ function parseRepoConfig(text2, file) {
|
|
|
2187
2218
|
rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
|
|
2188
2219
|
rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
|
|
2189
2220
|
rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
|
|
2221
|
+
rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup"]), "sandbox", file);
|
|
2190
2222
|
if (raw.version === void 0) throw new RepoConfigError(`${file}: version is required`);
|
|
2191
2223
|
if (raw.version !== SPEC_VERSION) {
|
|
2192
2224
|
throw new RepoConfigError(
|
|
@@ -2243,6 +2275,7 @@ function parseRepoConfig(text2, file) {
|
|
|
2243
2275
|
);
|
|
2244
2276
|
}
|
|
2245
2277
|
}
|
|
2278
|
+
const sandbox = parseSandbox(raw.sandbox, file);
|
|
2246
2279
|
const platform2 = String(raw.platform ?? "any");
|
|
2247
2280
|
if (!isPlatformRequirement(platform2)) {
|
|
2248
2281
|
throw new RepoConfigError(
|
|
@@ -2371,6 +2404,7 @@ function parseRepoConfig(text2, file) {
|
|
|
2371
2404
|
branch,
|
|
2372
2405
|
worktrees: worktrees2,
|
|
2373
2406
|
docs,
|
|
2407
|
+
...sandbox ? { sandbox } : {},
|
|
2374
2408
|
platform: platform2,
|
|
2375
2409
|
shell: raw.shell ? String(raw.shell) : void 0,
|
|
2376
2410
|
hooks,
|
|
@@ -2489,6 +2523,8 @@ function resolveRepoConfig(repo, ship, dir) {
|
|
|
2489
2523
|
);
|
|
2490
2524
|
return {
|
|
2491
2525
|
version: SPEC_VERSION,
|
|
2526
|
+
// Repo-only, like platform: what a repo needs installed is a fact about it.
|
|
2527
|
+
...repo?.sandbox ? { sandbox: repo.sandbox } : {},
|
|
2492
2528
|
worktrees: {
|
|
2493
2529
|
// Not merged: which gitignored files a worktree needs is a fact about
|
|
2494
2530
|
// the repository and nothing else, so a ship cannot supply or override
|
|
@@ -2678,6 +2714,23 @@ function parseCloneScheme(v) {
|
|
|
2678
2714
|
if (v === "ssh" || v === "https") return v;
|
|
2679
2715
|
throw new ConfigError(`ship.cloneScheme must be "ssh" or "https", got ${JSON.stringify(v)}`);
|
|
2680
2716
|
}
|
|
2717
|
+
function parseShipRunner(v) {
|
|
2718
|
+
if (v === void 0 || v === null || v === "") return void 0;
|
|
2719
|
+
if (v === "host" || v === "sandbox") return v;
|
|
2720
|
+
throw new ConfigError(`ship.runner must be "host" or "sandbox", got ${JSON.stringify(v)}`);
|
|
2721
|
+
}
|
|
2722
|
+
var SANDBOX_KEYS = ["bin", "image", "policy", "cacheVolume", "mirrorDir", "workDir", "agentBin", "forgeTokenVar"];
|
|
2723
|
+
function parseShipSandbox(v) {
|
|
2724
|
+
if (v === void 0 || v === null) return void 0;
|
|
2725
|
+
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.sandbox must be a map of settings");
|
|
2726
|
+
const out = {};
|
|
2727
|
+
for (const [k, val] of Object.entries(v)) {
|
|
2728
|
+
if (!SANDBOX_KEYS.includes(k)) throw new ConfigError(`ship.sandbox: unknown setting "${k}" (known: ${SANDBOX_KEYS.join(", ")})`);
|
|
2729
|
+
if (typeof val !== "string" || !val) throw new ConfigError(`ship.sandbox.${k} must be a non-empty string`);
|
|
2730
|
+
out[k] = val;
|
|
2731
|
+
}
|
|
2732
|
+
return out;
|
|
2733
|
+
}
|
|
2681
2734
|
function parseShipEnv(v) {
|
|
2682
2735
|
if (v === void 0 || v === null) return void 0;
|
|
2683
2736
|
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.env must be a map of variable name to value");
|
|
@@ -3124,6 +3177,8 @@ Create the first of those:
|
|
|
3124
3177
|
if (dupe) missing.add(`routes: duplicate route "${dupe}" \u2014 a route addresses itself, so it must be unique`);
|
|
3125
3178
|
missing.throwIfAny(file);
|
|
3126
3179
|
configureSecretStore({ mode: secretStore, stateDir });
|
|
3180
|
+
const shipRunner = parseShipRunner(shipRaw.runner);
|
|
3181
|
+
const shipSandbox = parseShipSandbox(shipRaw.sandbox);
|
|
3127
3182
|
return {
|
|
3128
3183
|
ship: {
|
|
3129
3184
|
name: shipRaw.name ?? "this ship",
|
|
@@ -3133,6 +3188,7 @@ Create the first of those:
|
|
|
3133
3188
|
model: shipRaw.agent?.model ?? "claude-sonnet-5",
|
|
3134
3189
|
maxThinkingTokens
|
|
3135
3190
|
},
|
|
3191
|
+
...shipRunner ? { runner: shipRunner, ...shipSandbox ? { sandbox: shipSandbox } : {} } : {},
|
|
3136
3192
|
...toolchains.length > 0 ? { toolchains } : {},
|
|
3137
3193
|
...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
|
|
3138
3194
|
...pair ? { pair } : {},
|
|
@@ -3844,9 +3900,9 @@ function labelFor(crewHome, job) {
|
|
|
3844
3900
|
return crewHome;
|
|
3845
3901
|
}
|
|
3846
3902
|
})();
|
|
3847
|
-
const
|
|
3903
|
+
const hash2 = createHash3("sha1").update(real).digest("hex").slice(0, 8);
|
|
3848
3904
|
const stem = job === "release" ? "com.tablation.crew-release" : job === "passengers" ? "com.tablation.crew-passengers" : "com.tablation.crew";
|
|
3849
|
-
return `${stem}.${
|
|
3905
|
+
return `${stem}.${hash2}`;
|
|
3850
3906
|
}
|
|
3851
3907
|
function cronMarkerFor(label) {
|
|
3852
3908
|
return `# crew:${label} \u2014 managed by \`crew install\`, do not edit by hand`;
|
|
@@ -5293,11 +5349,433 @@ function attentionProblems(items, already) {
|
|
|
5293
5349
|
return items.filter((i) => !seen.has(i.key)).map((i) => ({ severity: "attention", kind: i.kind, message: i.message }));
|
|
5294
5350
|
}
|
|
5295
5351
|
|
|
5296
|
-
// src/
|
|
5352
|
+
// src/sandbox-image.ts
|
|
5353
|
+
import { createHash as createHash5 } from "crypto";
|
|
5354
|
+
import { spawn as spawn2 } from "child_process";
|
|
5355
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync3, writeFileSync as writeFileSync10, rmSync as rmSync9 } from "fs";
|
|
5356
|
+
import { join as join15 } from "path";
|
|
5357
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5358
|
+
|
|
5359
|
+
// src/passenger-containers.ts
|
|
5297
5360
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
5361
|
+
import { createHash as createHash4 } from "crypto";
|
|
5362
|
+
var realExec = (cmd, args, opts) => {
|
|
5363
|
+
try {
|
|
5364
|
+
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
5365
|
+
} catch (e) {
|
|
5366
|
+
const err3 = e;
|
|
5367
|
+
const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
5368
|
+
throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
5369
|
+
}
|
|
5370
|
+
};
|
|
5371
|
+
var DOCKER_INFO_TIMEOUT_MS = 1e4;
|
|
5372
|
+
function dockerAvailable(exec = realExec) {
|
|
5373
|
+
try {
|
|
5374
|
+
exec("docker", ["info"], { timeoutMs: DOCKER_INFO_TIMEOUT_MS });
|
|
5375
|
+
return true;
|
|
5376
|
+
} catch {
|
|
5377
|
+
return false;
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
|
|
5381
|
+
function passengerImageRef(cfg) {
|
|
5382
|
+
return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
|
|
5383
|
+
}
|
|
5384
|
+
function imagePresent(image, exec = realExec) {
|
|
5385
|
+
try {
|
|
5386
|
+
exec("docker", ["image", "inspect", image]);
|
|
5387
|
+
return true;
|
|
5388
|
+
} catch {
|
|
5389
|
+
return false;
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
function ensureImage(image, exec = realExec) {
|
|
5393
|
+
if (!imagePresent(image, exec)) exec("docker", ["pull", image]);
|
|
5394
|
+
}
|
|
5395
|
+
function containerNameFor(workspaceId) {
|
|
5396
|
+
return `crew-passenger-${workspaceId}`;
|
|
5397
|
+
}
|
|
5398
|
+
function portFor(workspaceId, base = 28765, span = 1e3) {
|
|
5399
|
+
const digest = createHash4("sha256").update(workspaceId).digest();
|
|
5400
|
+
return base + digest.readUInt32BE(0) % span;
|
|
5401
|
+
}
|
|
5402
|
+
function mountsDigest(mounts, image) {
|
|
5403
|
+
const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
|
|
5404
|
+
return createHash4("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
5405
|
+
}
|
|
5406
|
+
function planContainers(cfg) {
|
|
5407
|
+
const byWorkspace = /* @__PURE__ */ new Map();
|
|
5408
|
+
const image = passengerImageRef(cfg);
|
|
5409
|
+
for (const route2 of cfg.routes) {
|
|
5410
|
+
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
5411
|
+
const workspaceId = route2.resolved.workspaceId;
|
|
5412
|
+
const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
|
|
5413
|
+
for (const target of passengerCheckoutTargets(route2)) {
|
|
5414
|
+
mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
|
|
5415
|
+
}
|
|
5416
|
+
byWorkspace.set(workspaceId, mounts);
|
|
5417
|
+
}
|
|
5418
|
+
return [...byWorkspace.entries()].filter(([, mounts]) => mounts.size > 0).map(([workspaceId, mounts]) => {
|
|
5419
|
+
const mountList = [...mounts.values()];
|
|
5420
|
+
return {
|
|
5421
|
+
workspaceId,
|
|
5422
|
+
containerName: containerNameFor(workspaceId),
|
|
5423
|
+
port: portFor(workspaceId),
|
|
5424
|
+
image,
|
|
5425
|
+
mounts: mountList,
|
|
5426
|
+
mountsHash: mountsDigest(mountList, image)
|
|
5427
|
+
};
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
function listPassengerContainers(exec = realExec) {
|
|
5431
|
+
const out = exec("docker", ["ps", "-a", "--filter", "name=crew-passenger-", "--format", "{{.Names}} {{.State}}"]);
|
|
5432
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
|
|
5433
|
+
const [name, state] = line.split(" ");
|
|
5434
|
+
return { name, running: state === "running" };
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
function currentMountsHash(name, exec) {
|
|
5438
|
+
try {
|
|
5439
|
+
const out = exec("docker", ["inspect", "--format", '{{ index .Config.Labels "crew.mounts.hash" }}', name]);
|
|
5440
|
+
const hash2 = out.trim();
|
|
5441
|
+
return hash2 || void 0;
|
|
5442
|
+
} catch {
|
|
5443
|
+
return void 0;
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
function stopContainer(name, exec = realExec) {
|
|
5447
|
+
try {
|
|
5448
|
+
exec("docker", ["rm", "-f", name]);
|
|
5449
|
+
} catch {
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
function startContainer(plan, tablationApiBaseUrl, sync, exec = realExec) {
|
|
5453
|
+
const args = [
|
|
5454
|
+
"run",
|
|
5455
|
+
"-d",
|
|
5456
|
+
"--name",
|
|
5457
|
+
plan.containerName,
|
|
5458
|
+
"--restart",
|
|
5459
|
+
"unless-stopped",
|
|
5460
|
+
"-p",
|
|
5461
|
+
`${plan.port}:8765`,
|
|
5462
|
+
"-e",
|
|
5463
|
+
`PASSENGER_MCP_WORKSPACE_ID=${plan.workspaceId}`,
|
|
5464
|
+
"-e",
|
|
5465
|
+
`TABLATION_API_BASE_URL=${tablationApiBaseUrl}`,
|
|
5466
|
+
"--label",
|
|
5467
|
+
`crew.mounts.hash=${plan.mountsHash}`
|
|
5468
|
+
];
|
|
5469
|
+
if (sync) {
|
|
5470
|
+
args.push(
|
|
5471
|
+
"--add-host",
|
|
5472
|
+
"host.docker.internal:host-gateway",
|
|
5473
|
+
"-e",
|
|
5474
|
+
`PASSENGER_SYNC_URL=http://${sync.host}:${sync.port}/sync`,
|
|
5475
|
+
"-e",
|
|
5476
|
+
`PASSENGER_SYNC_SECRET=${sync.secret}`,
|
|
5477
|
+
"-e",
|
|
5478
|
+
`PASSENGER_MCP_SECRET=${sync.secret}`
|
|
5479
|
+
);
|
|
5480
|
+
}
|
|
5481
|
+
for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
|
|
5482
|
+
args.push(plan.image);
|
|
5483
|
+
exec("docker", args);
|
|
5484
|
+
}
|
|
5485
|
+
function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
|
|
5486
|
+
const plans = planContainers(cfg);
|
|
5487
|
+
const plannedByName = new Map(plans.map((p) => [p.containerName, p]));
|
|
5488
|
+
const existing = listPassengerContainers(exec);
|
|
5489
|
+
const existingByName = new Map(existing.map((c) => [c.name, c]));
|
|
5490
|
+
const started = [];
|
|
5491
|
+
const recreated = [];
|
|
5492
|
+
const stopped = [];
|
|
5493
|
+
const ensured = /* @__PURE__ */ new Set();
|
|
5494
|
+
const start = (plan, sync) => {
|
|
5495
|
+
if (!ensured.has(plan.image)) {
|
|
5496
|
+
ensureImage(plan.image, exec);
|
|
5497
|
+
ensured.add(plan.image);
|
|
5498
|
+
}
|
|
5499
|
+
startContainer(plan, tablationApiBaseUrl, sync, exec);
|
|
5500
|
+
};
|
|
5501
|
+
for (const plan of plans) {
|
|
5502
|
+
const sync = syncEndpoints.get(plan.workspaceId);
|
|
5503
|
+
const current = existingByName.get(plan.containerName);
|
|
5504
|
+
if (!current) {
|
|
5505
|
+
start(plan, sync);
|
|
5506
|
+
started.push(plan.containerName);
|
|
5507
|
+
} else if (!current.running) {
|
|
5508
|
+
stopContainer(plan.containerName, exec);
|
|
5509
|
+
start(plan, sync);
|
|
5510
|
+
started.push(plan.containerName);
|
|
5511
|
+
} else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
|
|
5512
|
+
stopContainer(plan.containerName, exec);
|
|
5513
|
+
start(plan, sync);
|
|
5514
|
+
recreated.push(plan.containerName);
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
for (const { name } of existing) {
|
|
5518
|
+
if (!plannedByName.has(name)) {
|
|
5519
|
+
stopContainer(name, exec);
|
|
5520
|
+
stopped.push(name);
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
return { started, recreated, stopped };
|
|
5524
|
+
}
|
|
5525
|
+
function passengerCheckoutSyncSpecs(cfg, workspaceId) {
|
|
5526
|
+
const specs = [];
|
|
5527
|
+
for (const route2 of cfg.routes) {
|
|
5528
|
+
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
5529
|
+
if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
|
|
5530
|
+
const routeWorkspaceId = route2.resolved.workspaceId;
|
|
5531
|
+
const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
|
|
5532
|
+
for (const target of passengerCheckoutTargets(route2)) {
|
|
5533
|
+
const ordinary = ordinaryByName.get(target.name);
|
|
5534
|
+
const override = route2.repoOverrides[target.name];
|
|
5535
|
+
const effective = resolveRepoConfig(
|
|
5536
|
+
ordinary ? loadRepoConfig(ordinary.dir) : null,
|
|
5537
|
+
{ branch: { ...route2.branch, ...override?.branch } },
|
|
5538
|
+
ordinary?.dir ?? target.dir
|
|
5539
|
+
);
|
|
5540
|
+
specs.push({
|
|
5541
|
+
name: target.name,
|
|
5542
|
+
workspaceId: routeWorkspaceId,
|
|
5543
|
+
dir: target.dir,
|
|
5544
|
+
remote: target.remote,
|
|
5545
|
+
gitRemoteName: effective.branch.remote,
|
|
5546
|
+
base: effective.branch.base,
|
|
5547
|
+
originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
|
|
5548
|
+
cloneScheme: cfg.ship.cloneScheme,
|
|
5549
|
+
...target.forgeHost ? { forgeHost: target.forgeHost } : {}
|
|
5550
|
+
});
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
return specs;
|
|
5554
|
+
}
|
|
5555
|
+
function syncAllPassengerCheckouts(cfg) {
|
|
5556
|
+
return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
|
|
5557
|
+
name: spec.name,
|
|
5558
|
+
workspaceId: spec.workspaceId,
|
|
5559
|
+
outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
|
|
5560
|
+
}));
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
// src/sandbox-image.ts
|
|
5564
|
+
var SANDBOX_BASE_IMAGE = "nvcr.io/nvidia/base/ubuntu:24.04";
|
|
5565
|
+
var SANDBOX_HARNESS_VERSION = "0.1.6";
|
|
5566
|
+
var SANDBOX_UID = 1e3;
|
|
5567
|
+
var CREW_PREFIX = "/opt/crew";
|
|
5568
|
+
var SANDBOX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
5569
|
+
var KEEP_TAGS = 2;
|
|
5570
|
+
var CREDENTIAL_HELPER = `#!/bin/sh
|
|
5571
|
+
# git credential helper: answers from the environment the runner passes in, so
|
|
5572
|
+
# no token is ever written into an image layer.
|
|
5573
|
+
[ "$1" = get ] || exit 0
|
|
5574
|
+
[ -n "$CREW_FORGE_TOKEN" ] || exit 0
|
|
5575
|
+
echo "username=\${CREW_FORGE_USER:-x-access-token}"
|
|
5576
|
+
echo "password=$CREW_FORGE_TOKEN"
|
|
5577
|
+
`;
|
|
5578
|
+
var MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
|
|
5579
|
+
`;
|
|
5580
|
+
var hash = (parts) => createHash5("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 12);
|
|
5581
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
5582
|
+
var run = (cmd) => `RUN ["/bin/bash", "-c", ${JSON.stringify(`set -euo pipefail; ${cmd}`)}]`;
|
|
5583
|
+
var CREW_ENV = {
|
|
5584
|
+
CREW_SANDBOX: "1",
|
|
5585
|
+
CREW_CACHE_DIR: `${CREW_PREFIX}/cache`,
|
|
5586
|
+
XDG_CACHE_HOME: `${CREW_PREFIX}/cache`,
|
|
5587
|
+
npm_config_cache: `${CREW_PREFIX}/cache/npm`,
|
|
5588
|
+
npm_config_store_dir: `${CREW_PREFIX}/pnpm-store`,
|
|
5589
|
+
PNPM_HOME: `${CREW_PREFIX}/pnpm`
|
|
5590
|
+
};
|
|
5591
|
+
function baseLayer() {
|
|
5592
|
+
const dockerfile = [
|
|
5593
|
+
`FROM ${SANDBOX_BASE_IMAGE}`,
|
|
5594
|
+
`LABEL io.crew.sandbox.layer="base" io.crew.sandbox.harness="${SANDBOX_HARNESS_VERSION}"`,
|
|
5595
|
+
""
|
|
5596
|
+
].join("\n");
|
|
5597
|
+
return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_HARNESS_VERSION })}`, dockerfile, files: {} };
|
|
5598
|
+
}
|
|
5599
|
+
function crewLayer(base) {
|
|
5600
|
+
const files = { "credential-helper": CREDENTIAL_HELPER, "mcp.json": MCP_CONFIG };
|
|
5601
|
+
const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
5602
|
+
const dockerfile = [
|
|
5603
|
+
`FROM ${base.tag}`,
|
|
5604
|
+
"USER root",
|
|
5605
|
+
run("apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*"),
|
|
5606
|
+
`COPY credential-helper ${CREW_PREFIX}/bin/credential-helper`,
|
|
5607
|
+
`COPY mcp.json ${CREW_PREFIX}/mcp/mcp.json`,
|
|
5608
|
+
run(`chmod 0755 ${CREW_PREFIX}/bin/credential-helper && git config --system credential.helper ${CREW_PREFIX}/bin/credential-helper`),
|
|
5609
|
+
run(`mkdir -p ${CREW_PREFIX}/cache ${CREW_PREFIX}/pnpm-store ${CREW_PREFIX}/pnpm /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} ${CREW_PREFIX} /sandbox`),
|
|
5610
|
+
`ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
5611
|
+
`LABEL io.crew.sandbox.layer="crew"`,
|
|
5612
|
+
""
|
|
5613
|
+
].join("\n");
|
|
5614
|
+
return { name: "crew", tag: `crew-sandbox-crew:${hash({ base: base.tag, dockerfile, files })}`, dockerfile, files };
|
|
5615
|
+
}
|
|
5616
|
+
function repoLayer(repo, decl, crew) {
|
|
5617
|
+
const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
5618
|
+
const lines2 = [];
|
|
5619
|
+
if (decl.image) {
|
|
5620
|
+
lines2.push(
|
|
5621
|
+
`FROM ${crew.tag} AS crew`,
|
|
5622
|
+
`FROM ${decl.image}`,
|
|
5623
|
+
"USER root",
|
|
5624
|
+
`COPY --from=crew ${CREW_PREFIX} ${CREW_PREFIX}`,
|
|
5625
|
+
`ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:$PATH`
|
|
5626
|
+
);
|
|
5627
|
+
} else {
|
|
5628
|
+
lines2.push(`FROM ${crew.tag}`, "USER root");
|
|
5629
|
+
}
|
|
5630
|
+
if (decl.packages.length > 0) {
|
|
5631
|
+
lines2.push(run(`apt-get update && apt-get install -y --no-install-recommends ${decl.packages.join(" ")} && rm -rf /var/lib/apt/lists/*`));
|
|
5632
|
+
}
|
|
5633
|
+
lines2.push(
|
|
5634
|
+
run(`mkdir -p /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} /sandbox ${CREW_PREFIX}`),
|
|
5635
|
+
`USER ${SANDBOX_UID}`,
|
|
5636
|
+
"WORKDIR /sandbox"
|
|
5637
|
+
);
|
|
5638
|
+
for (const step of decl.setup) lines2.push(run(step));
|
|
5639
|
+
lines2.push(`LABEL io.crew.sandbox.layer="repo" io.crew.sandbox.repo=${JSON.stringify(repo)}`, "");
|
|
5640
|
+
const dockerfile = lines2.join("\n");
|
|
5641
|
+
return { name: "repo", tag: `crew-sandbox/${slug(repo)}:${hash({ crew: crew.tag, dockerfile })}`, dockerfile, files: {} };
|
|
5642
|
+
}
|
|
5643
|
+
function planSandbox(repo, decl) {
|
|
5644
|
+
const base = baseLayer();
|
|
5645
|
+
const crew = crewLayer(base);
|
|
5646
|
+
const layer = repoLayer(repo, decl, crew);
|
|
5647
|
+
return { repo, tag: layer.tag, layers: [base, crew, layer] };
|
|
5648
|
+
}
|
|
5649
|
+
var sandboxStatePath = (stateDir) => join15(stateDir, "sandbox-images.json");
|
|
5650
|
+
function readSandboxState(stateDir) {
|
|
5651
|
+
try {
|
|
5652
|
+
const v = JSON.parse(readFileSync14(sandboxStatePath(stateDir), "utf8"));
|
|
5653
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
5654
|
+
} catch {
|
|
5655
|
+
return {};
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
function writeSandboxState(stateDir, state) {
|
|
5659
|
+
mkdirSync9(stateDir, { recursive: true });
|
|
5660
|
+
const file = sandboxStatePath(stateDir);
|
|
5661
|
+
writeFileSync10(`${file}.tmp`, `${JSON.stringify(state, null, 2)}
|
|
5662
|
+
`);
|
|
5663
|
+
renameSync3(`${file}.tmp`, file);
|
|
5664
|
+
}
|
|
5665
|
+
function sandboxImageStatus(state, plan) {
|
|
5666
|
+
const s = state[plan.repo];
|
|
5667
|
+
const current = s?.tags[0];
|
|
5668
|
+
if (s?.failure && s.failure.tag === plan.tag) {
|
|
5669
|
+
return { status: "failed", detail: `last build of ${plan.tag} failed at ${s.failure.at}; log ${s.failure.logPath}${current ? `; running ${current.tag}` : ""}` };
|
|
5670
|
+
}
|
|
5671
|
+
if (!current) return { status: "missing", detail: `no image built yet (wants ${plan.tag}); run \`crew sandbox build\`` };
|
|
5672
|
+
if (current.tag !== plan.tag) return { status: "stale", detail: `running ${current.tag}, declaration now builds ${plan.tag}` };
|
|
5673
|
+
return { status: "ok", detail: `${current.tag} built ${current.builtAt}` };
|
|
5674
|
+
}
|
|
5675
|
+
var spawnBuildRunner = (engine = "docker") => (args, logPath) => new Promise((resolve7) => {
|
|
5676
|
+
const child = spawn2(engine, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5677
|
+
const chunks = [];
|
|
5678
|
+
child.stdout.on("data", (d) => chunks.push(d));
|
|
5679
|
+
child.stderr.on("data", (d) => chunks.push(d));
|
|
5680
|
+
const done = (code, extra = "") => {
|
|
5681
|
+
writeFileSync10(logPath, `$ ${engine} ${args.join(" ")}
|
|
5682
|
+
${Buffer.concat(chunks).toString("utf8")}${extra}`, { flag: "a" });
|
|
5683
|
+
resolve7(code);
|
|
5684
|
+
};
|
|
5685
|
+
child.on("error", (e) => done(127, `${e.message}
|
|
5686
|
+
`));
|
|
5687
|
+
child.on("close", (code) => done(code ?? 1));
|
|
5688
|
+
});
|
|
5689
|
+
var engineHasImage = (engine = "docker", exec = realExec) => (tag) => {
|
|
5690
|
+
try {
|
|
5691
|
+
exec(engine, ["image", "inspect", tag], { timeoutMs: 2e4 });
|
|
5692
|
+
return true;
|
|
5693
|
+
} catch {
|
|
5694
|
+
return false;
|
|
5695
|
+
}
|
|
5696
|
+
};
|
|
5697
|
+
var engineRemoveImage = (engine = "docker", exec = realExec) => (tag) => {
|
|
5698
|
+
try {
|
|
5699
|
+
exec(engine, ["image", "rm", tag], { timeoutMs: 6e4 });
|
|
5700
|
+
} catch {
|
|
5701
|
+
}
|
|
5702
|
+
};
|
|
5703
|
+
var sandboxLogDir = (stateDir) => join15(stateDir, "sandbox-logs");
|
|
5704
|
+
async function buildSandboxImage(plan, deps) {
|
|
5705
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
5706
|
+
const state = readSandboxState(deps.stateDir);
|
|
5707
|
+
const entry = state[plan.repo] ?? { tags: [] };
|
|
5708
|
+
if (entry.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag)) {
|
|
5709
|
+
return { kind: "reused", tag: plan.tag };
|
|
5710
|
+
}
|
|
5711
|
+
const logDir = sandboxLogDir(deps.stateDir);
|
|
5712
|
+
mkdirSync9(logDir, { recursive: true });
|
|
5713
|
+
const logPath = join15(logDir, `${slug(plan.repo)}-${now().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
5714
|
+
const builtLayers = [];
|
|
5715
|
+
for (const layer of plan.layers) {
|
|
5716
|
+
if (deps.hasImage(layer.tag)) continue;
|
|
5717
|
+
const ctx = join15(tmpdir2(), `crew-sandbox-${hash([layer.tag, process.pid, Date.now()])}`);
|
|
5718
|
+
mkdirSync9(ctx, { recursive: true });
|
|
5719
|
+
try {
|
|
5720
|
+
writeFileSync10(join15(ctx, "Dockerfile"), layer.dockerfile);
|
|
5721
|
+
for (const [name, content] of Object.entries(layer.files)) writeFileSync10(join15(ctx, name), content);
|
|
5722
|
+
const code = await deps.run(["build", "--tag", layer.tag, "--file", join15(ctx, "Dockerfile"), ctx], logPath);
|
|
5723
|
+
if (code !== 0) {
|
|
5724
|
+
state[plan.repo] = { ...entry, failure: { tag: plan.tag, at: now().toISOString(), logPath } };
|
|
5725
|
+
writeSandboxState(deps.stateDir, state);
|
|
5726
|
+
return { kind: "failed", tag: plan.tag, logPath, layer: layer.name };
|
|
5727
|
+
}
|
|
5728
|
+
builtLayers.push(layer.name);
|
|
5729
|
+
} finally {
|
|
5730
|
+
rmSync9(ctx, { recursive: true, force: true });
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
const tags = [{ tag: plan.tag, builtAt: now().toISOString() }, ...entry.tags.filter((t) => t.tag !== plan.tag)];
|
|
5734
|
+
const dropped = tags.slice(KEEP_TAGS);
|
|
5735
|
+
state[plan.repo] = { tags: tags.slice(0, KEEP_TAGS) };
|
|
5736
|
+
writeSandboxState(deps.stateDir, state);
|
|
5737
|
+
for (const t of dropped) deps.removeImage(t.tag);
|
|
5738
|
+
return { kind: "built", tag: plan.tag, builtLayers };
|
|
5739
|
+
}
|
|
5740
|
+
var sandboxFailedKey = (repo) => `sandbox_build_failed:${repo}`;
|
|
5741
|
+
function sandboxFailedItem(repo, tag, layer, logPath, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5742
|
+
return {
|
|
5743
|
+
kind: "sandbox_build_failed",
|
|
5744
|
+
key: sandboxFailedKey(repo),
|
|
5745
|
+
since: nowIso,
|
|
5746
|
+
message: `${repo}: sandbox image ${tag} failed to build at the ${layer} layer (log: ${logPath}); sandboxes keep running the previous image. Retries automatically after the declaration changes or in an hour; \`crew sandbox build\` forces it`
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
async function sandboxBuildTick(repos, deps, force = false) {
|
|
5750
|
+
const nowMs = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
5751
|
+
for (const r of repos) {
|
|
5752
|
+
const plan = planSandbox(r.name, r.decl);
|
|
5753
|
+
const state = readSandboxState(deps.stateDir)[r.name];
|
|
5754
|
+
const upToDate = state?.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag);
|
|
5755
|
+
if (upToDate) {
|
|
5756
|
+
deps.clear(sandboxFailedKey(r.name));
|
|
5757
|
+
continue;
|
|
5758
|
+
}
|
|
5759
|
+
if (!force && state?.failure?.tag === plan.tag && nowMs - Date.parse(state.failure.at) < SANDBOX_RETRY_AFTER_MS) continue;
|
|
5760
|
+
deps.log(`sandbox image for ${r.name}: building ${plan.tag}`);
|
|
5761
|
+
const out = await buildSandboxImage(plan, deps);
|
|
5762
|
+
if (out.kind === "failed") {
|
|
5763
|
+
deps.raise(sandboxFailedItem(r.name, out.tag, out.layer, out.logPath));
|
|
5764
|
+
deps.log(`sandbox image for ${r.name}: build failed at the ${out.layer} layer, log ${out.logPath}`);
|
|
5765
|
+
} else {
|
|
5766
|
+
deps.clear(sandboxFailedKey(r.name));
|
|
5767
|
+
deps.log(`sandbox image for ${r.name}: ${out.kind === "built" ? `built ${out.tag}` : `${out.tag} already built`}`);
|
|
5768
|
+
}
|
|
5769
|
+
return r.name;
|
|
5770
|
+
}
|
|
5771
|
+
return null;
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5774
|
+
// src/doctor-checks.ts
|
|
5775
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5298
5776
|
var PROBE_BRANCH = "crew-probe-branch";
|
|
5299
5777
|
var realGit = (cwd, args, env) => {
|
|
5300
|
-
|
|
5778
|
+
execFileSync4("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
|
|
5301
5779
|
};
|
|
5302
5780
|
function isSshRemote(url) {
|
|
5303
5781
|
return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
|
|
@@ -5305,12 +5783,12 @@ function isSshRemote(url) {
|
|
|
5305
5783
|
function pushAuthHint(remoteUrl) {
|
|
5306
5784
|
return isSshRemote(remoteUrl) ? "make sure an SSH key is loaded for this host (ssh-add), and that the scheduler user can reach it" : "run `gh auth login`, then `gh auth setup-git`";
|
|
5307
5785
|
}
|
|
5308
|
-
function probePushAuth(o,
|
|
5786
|
+
function probePushAuth(o, run3 = realGit, baseEnv = process.env) {
|
|
5309
5787
|
if (o.offline) return { state: "skipped", detail: "skipped (--offline)" };
|
|
5310
5788
|
if (!o.remoteUrl) return { state: "skipped", detail: `no remote "${o.remote}" configured` };
|
|
5311
5789
|
const env = { ...baseEnv, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_SSH_COMMAND: `${baseEnv.GIT_SSH_COMMAND ?? "ssh"} -o BatchMode=yes` };
|
|
5312
5790
|
try {
|
|
5313
|
-
|
|
5791
|
+
run3(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
|
|
5314
5792
|
return { state: "ok" };
|
|
5315
5793
|
} catch (e) {
|
|
5316
5794
|
const err3 = e;
|
|
@@ -5326,7 +5804,7 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
|
|
|
5326
5804
|
};
|
|
5327
5805
|
}
|
|
5328
5806
|
try {
|
|
5329
|
-
|
|
5807
|
+
run3(o.dir, ["ls-remote", "--exit-code", o.remote, "HEAD"], env);
|
|
5330
5808
|
return {
|
|
5331
5809
|
state: "failed",
|
|
5332
5810
|
detail: `the token for forge ${f.host} is read-only (fetch works, push refused): ${detail}`,
|
|
@@ -5418,31 +5896,31 @@ function roleCapWarnings(ship) {
|
|
|
5418
5896
|
}
|
|
5419
5897
|
|
|
5420
5898
|
// src/selftest.ts
|
|
5421
|
-
import { spawn as
|
|
5422
|
-
import { existsSync as existsSync22, mkdirSync as
|
|
5423
|
-
import { tmpdir as
|
|
5424
|
-
import { join as
|
|
5899
|
+
import { spawn as spawn5 } from "child_process";
|
|
5900
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, readFileSync as readFileSync21, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
|
|
5901
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
5902
|
+
import { join as join25 } from "path";
|
|
5425
5903
|
|
|
5426
5904
|
// src/agent.ts
|
|
5427
|
-
import {
|
|
5428
|
-
import { readFileSync as
|
|
5905
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
5906
|
+
import { readFileSync as readFileSync20, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
5429
5907
|
import { StringDecoder } from "string_decoder";
|
|
5430
5908
|
import { finished } from "stream/promises";
|
|
5431
|
-
import { join as
|
|
5432
|
-
import { createHash as
|
|
5909
|
+
import { join as join24, dirname as dirname8, basename as basename8 } from "path";
|
|
5910
|
+
import { createHash as createHash6 } from "crypto";
|
|
5433
5911
|
|
|
5434
5912
|
// src/state.ts
|
|
5435
|
-
import { readFileSync as
|
|
5436
|
-
import { join as
|
|
5913
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync10, rmSync as rmSync10, statSync as statSync6, readdirSync as readdirSync5 } from "fs";
|
|
5914
|
+
import { join as join16 } from "path";
|
|
5437
5915
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
5438
5916
|
var State = class _State {
|
|
5439
5917
|
dir;
|
|
5440
5918
|
constructor(dir) {
|
|
5441
5919
|
this.dir = dir;
|
|
5442
|
-
|
|
5920
|
+
mkdirSync10(dir, { recursive: true });
|
|
5443
5921
|
}
|
|
5444
5922
|
path(...parts) {
|
|
5445
|
-
return
|
|
5923
|
+
return join16(this.dir, ...parts);
|
|
5446
5924
|
}
|
|
5447
5925
|
/**
|
|
5448
5926
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -5453,14 +5931,14 @@ var State = class _State {
|
|
|
5453
5931
|
*/
|
|
5454
5932
|
watermark() {
|
|
5455
5933
|
try {
|
|
5456
|
-
const v =
|
|
5934
|
+
const v = readFileSync15(this.path(".poll-watermark"), "utf8").trim();
|
|
5457
5935
|
return v || EPOCH;
|
|
5458
5936
|
} catch {
|
|
5459
5937
|
return EPOCH;
|
|
5460
5938
|
}
|
|
5461
5939
|
}
|
|
5462
5940
|
setWatermark(at) {
|
|
5463
|
-
|
|
5941
|
+
writeFileSync11(this.path(".poll-watermark"), `${at}
|
|
5464
5942
|
`);
|
|
5465
5943
|
}
|
|
5466
5944
|
/**
|
|
@@ -5487,10 +5965,10 @@ var State = class _State {
|
|
|
5487
5965
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
5488
5966
|
}
|
|
5489
5967
|
pause(role) {
|
|
5490
|
-
|
|
5968
|
+
writeFileSync11(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
5491
5969
|
}
|
|
5492
5970
|
resume(role) {
|
|
5493
|
-
|
|
5971
|
+
rmSync10(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
5494
5972
|
}
|
|
5495
5973
|
/**
|
|
5496
5974
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -5501,8 +5979,8 @@ var State = class _State {
|
|
|
5501
5979
|
return existsSync14(this.path(".release-paused"));
|
|
5502
5980
|
}
|
|
5503
5981
|
setReleasePaused(paused) {
|
|
5504
|
-
if (paused)
|
|
5505
|
-
else
|
|
5982
|
+
if (paused) writeFileSync11(this.path(".release-paused"), "");
|
|
5983
|
+
else rmSync10(this.path(".release-paused"), { force: true });
|
|
5506
5984
|
}
|
|
5507
5985
|
/**
|
|
5508
5986
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -5539,7 +6017,7 @@ var State = class _State {
|
|
|
5539
6017
|
const name = _State.safe(rawName);
|
|
5540
6018
|
const file = this.path(`.${name}.lock`);
|
|
5541
6019
|
try {
|
|
5542
|
-
const existing = Number.parseInt(
|
|
6020
|
+
const existing = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
|
|
5543
6021
|
if (Number.isFinite(existing) && existing > 0) {
|
|
5544
6022
|
try {
|
|
5545
6023
|
process.kill(existing, 0);
|
|
@@ -5549,13 +6027,13 @@ var State = class _State {
|
|
|
5549
6027
|
}
|
|
5550
6028
|
} catch {
|
|
5551
6029
|
}
|
|
5552
|
-
|
|
6030
|
+
writeFileSync11(file, `${process.pid}
|
|
5553
6031
|
`);
|
|
5554
6032
|
return {
|
|
5555
6033
|
ok: true,
|
|
5556
6034
|
release: () => {
|
|
5557
6035
|
try {
|
|
5558
|
-
|
|
6036
|
+
rmSync10(file, { force: true });
|
|
5559
6037
|
} catch {
|
|
5560
6038
|
}
|
|
5561
6039
|
}
|
|
@@ -5577,7 +6055,7 @@ var State = class _State {
|
|
|
5577
6055
|
if (!/^\.release-.+\.lock$/.test(n)) continue;
|
|
5578
6056
|
let pid = 0;
|
|
5579
6057
|
try {
|
|
5580
|
-
pid = Number.parseInt(
|
|
6058
|
+
pid = Number.parseInt(readFileSync15(this.path(n), "utf8").trim(), 10);
|
|
5581
6059
|
} catch {
|
|
5582
6060
|
continue;
|
|
5583
6061
|
}
|
|
@@ -5663,7 +6141,7 @@ var State = class _State {
|
|
|
5663
6141
|
ticketInFlight(recordId) {
|
|
5664
6142
|
const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
|
|
5665
6143
|
try {
|
|
5666
|
-
const pid = Number.parseInt(
|
|
6144
|
+
const pid = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
|
|
5667
6145
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
5668
6146
|
process.kill(pid, 0);
|
|
5669
6147
|
return true;
|
|
@@ -5697,7 +6175,7 @@ var State = class _State {
|
|
|
5697
6175
|
const heldReported = `.release-held-${suffix}`;
|
|
5698
6176
|
const read = (f) => {
|
|
5699
6177
|
try {
|
|
5700
|
-
return
|
|
6178
|
+
return readFileSync15(this.path(f), "utf8").trim() || null;
|
|
5701
6179
|
} catch {
|
|
5702
6180
|
return null;
|
|
5703
6181
|
}
|
|
@@ -5706,12 +6184,12 @@ var State = class _State {
|
|
|
5706
6184
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
5707
6185
|
noteBlocked: () => {
|
|
5708
6186
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
5709
|
-
|
|
6187
|
+
writeFileSync11(this.path(blocked), `${n}
|
|
5710
6188
|
`);
|
|
5711
6189
|
return n;
|
|
5712
6190
|
},
|
|
5713
6191
|
clearBlocked: () => {
|
|
5714
|
-
|
|
6192
|
+
rmSync10(this.path(blocked), { force: true });
|
|
5715
6193
|
},
|
|
5716
6194
|
/**
|
|
5717
6195
|
* The commit a deploy last failed on.
|
|
@@ -5723,11 +6201,11 @@ var State = class _State {
|
|
|
5723
6201
|
*/
|
|
5724
6202
|
deployFailedSha: () => read(failed),
|
|
5725
6203
|
noteDeployFailed: (sha) => {
|
|
5726
|
-
|
|
6204
|
+
writeFileSync11(this.path(failed), `${sha}
|
|
5727
6205
|
`);
|
|
5728
6206
|
},
|
|
5729
6207
|
clearDeployFailed: () => {
|
|
5730
|
-
|
|
6208
|
+
rmSync10(this.path(failed), { force: true });
|
|
5731
6209
|
},
|
|
5732
6210
|
/**
|
|
5733
6211
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -5737,31 +6215,31 @@ var State = class _State {
|
|
|
5737
6215
|
noteTestGateFailed: (sha) => {
|
|
5738
6216
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
5739
6217
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
5740
|
-
|
|
6218
|
+
writeFileSync11(this.path(gateFailed), `${sha} ${next}
|
|
5741
6219
|
`);
|
|
5742
6220
|
return next;
|
|
5743
6221
|
},
|
|
5744
6222
|
clearTestGateFailed: () => {
|
|
5745
|
-
|
|
6223
|
+
rmSync10(this.path(gateFailed), { force: true });
|
|
5746
6224
|
},
|
|
5747
6225
|
/** Whether the board was already told about this head's red gate. */
|
|
5748
6226
|
heldReported: () => {
|
|
5749
6227
|
try {
|
|
5750
|
-
return
|
|
6228
|
+
return readFileSync15(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
5751
6229
|
} catch {
|
|
5752
6230
|
return null;
|
|
5753
6231
|
}
|
|
5754
6232
|
},
|
|
5755
6233
|
noteHeldReported: (signature) => {
|
|
5756
|
-
|
|
6234
|
+
writeFileSync11(this.path(heldReported), `${signature}
|
|
5757
6235
|
`);
|
|
5758
6236
|
},
|
|
5759
6237
|
clearHeldReported: () => {
|
|
5760
|
-
|
|
6238
|
+
rmSync10(this.path(heldReported), { force: true });
|
|
5761
6239
|
},
|
|
5762
6240
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
5763
6241
|
noteTestGateReported: (sha) => {
|
|
5764
|
-
|
|
6242
|
+
writeFileSync11(this.path(gateReported), `${sha}
|
|
5765
6243
|
`);
|
|
5766
6244
|
}
|
|
5767
6245
|
};
|
|
@@ -5784,13 +6262,13 @@ var State = class _State {
|
|
|
5784
6262
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
5785
6263
|
const read = (f) => {
|
|
5786
6264
|
try {
|
|
5787
|
-
return
|
|
6265
|
+
return readFileSync15(this.path(f), "utf8").trim() || null;
|
|
5788
6266
|
} catch {
|
|
5789
6267
|
return null;
|
|
5790
6268
|
}
|
|
5791
6269
|
};
|
|
5792
6270
|
const clear2 = () => {
|
|
5793
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
6271
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync10(this.path(f), { force: true });
|
|
5794
6272
|
};
|
|
5795
6273
|
return {
|
|
5796
6274
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -5817,18 +6295,18 @@ var State = class _State {
|
|
|
5817
6295
|
clear2();
|
|
5818
6296
|
return;
|
|
5819
6297
|
}
|
|
5820
|
-
|
|
6298
|
+
writeFileSync11(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
5821
6299
|
`);
|
|
5822
6300
|
const prior = read(ticketFile);
|
|
5823
6301
|
if (topTicket && topTicket === prior) {
|
|
5824
|
-
|
|
6302
|
+
writeFileSync11(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
5825
6303
|
`);
|
|
5826
6304
|
} else if (topTicket) {
|
|
5827
|
-
|
|
6305
|
+
writeFileSync11(this.path(ticketFile), `${topTicket}
|
|
5828
6306
|
`);
|
|
5829
|
-
|
|
6307
|
+
writeFileSync11(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
5830
6308
|
`);
|
|
5831
|
-
|
|
6309
|
+
writeFileSync11(this.path(ticketStreakFile), "1\n");
|
|
5832
6310
|
}
|
|
5833
6311
|
}
|
|
5834
6312
|
};
|
|
@@ -5849,14 +6327,14 @@ var State = class _State {
|
|
|
5849
6327
|
return {
|
|
5850
6328
|
previous: () => {
|
|
5851
6329
|
try {
|
|
5852
|
-
const parsed = JSON.parse(
|
|
6330
|
+
const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
|
|
5853
6331
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5854
6332
|
} catch {
|
|
5855
6333
|
return {};
|
|
5856
6334
|
}
|
|
5857
6335
|
},
|
|
5858
6336
|
persist: (next) => {
|
|
5859
|
-
|
|
6337
|
+
writeFileSync11(this.path(file), JSON.stringify(next));
|
|
5860
6338
|
}
|
|
5861
6339
|
};
|
|
5862
6340
|
}
|
|
@@ -5872,14 +6350,14 @@ var State = class _State {
|
|
|
5872
6350
|
return {
|
|
5873
6351
|
previous: () => {
|
|
5874
6352
|
try {
|
|
5875
|
-
const parsed = JSON.parse(
|
|
6353
|
+
const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
|
|
5876
6354
|
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
|
|
5877
6355
|
} catch {
|
|
5878
6356
|
return [];
|
|
5879
6357
|
}
|
|
5880
6358
|
},
|
|
5881
6359
|
persist: (next) => {
|
|
5882
|
-
|
|
6360
|
+
writeFileSync11(this.path(file), JSON.stringify(next));
|
|
5883
6361
|
}
|
|
5884
6362
|
};
|
|
5885
6363
|
}
|
|
@@ -5893,14 +6371,14 @@ var State = class _State {
|
|
|
5893
6371
|
return {
|
|
5894
6372
|
previous: () => {
|
|
5895
6373
|
try {
|
|
5896
|
-
const parsed = JSON.parse(
|
|
6374
|
+
const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
|
|
5897
6375
|
return Array.isArray(parsed) ? parsed : [];
|
|
5898
6376
|
} catch {
|
|
5899
6377
|
return [];
|
|
5900
6378
|
}
|
|
5901
6379
|
},
|
|
5902
6380
|
persist: (next) => {
|
|
5903
|
-
|
|
6381
|
+
writeFileSync11(this.path(file), JSON.stringify(next));
|
|
5904
6382
|
}
|
|
5905
6383
|
};
|
|
5906
6384
|
}
|
|
@@ -5913,7 +6391,7 @@ var State = class _State {
|
|
|
5913
6391
|
const file = ".release-last-run";
|
|
5914
6392
|
return {
|
|
5915
6393
|
touch: () => {
|
|
5916
|
-
|
|
6394
|
+
writeFileSync11(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
5917
6395
|
},
|
|
5918
6396
|
at: () => {
|
|
5919
6397
|
try {
|
|
@@ -5926,7 +6404,7 @@ var State = class _State {
|
|
|
5926
6404
|
}
|
|
5927
6405
|
readInt(file) {
|
|
5928
6406
|
try {
|
|
5929
|
-
return Number.parseInt(
|
|
6407
|
+
return Number.parseInt(readFileSync15(this.path(file), "utf8").trim(), 10) || 0;
|
|
5930
6408
|
} catch {
|
|
5931
6409
|
return 0;
|
|
5932
6410
|
}
|
|
@@ -6409,8 +6887,8 @@ var PERISCOPE_COLUMNS = [
|
|
|
6409
6887
|
];
|
|
6410
6888
|
|
|
6411
6889
|
// src/agent-log.ts
|
|
6412
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
6413
|
-
import { join as
|
|
6890
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "fs";
|
|
6891
|
+
import { join as join17 } from "path";
|
|
6414
6892
|
|
|
6415
6893
|
// src/pending-backoff.ts
|
|
6416
6894
|
var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
|
|
@@ -6467,14 +6945,14 @@ async function findTicketRecord(target, t, ref) {
|
|
|
6467
6945
|
);
|
|
6468
6946
|
return rows.length === 1 ? rows[0].id : void 0;
|
|
6469
6947
|
}
|
|
6470
|
-
function counterIncrements(
|
|
6948
|
+
function counterIncrements(run3, cols, have) {
|
|
6471
6949
|
const figures = [
|
|
6472
6950
|
[cols.agentRuns, 1],
|
|
6473
|
-
[cols.tokensIn,
|
|
6474
|
-
[cols.tokensOut,
|
|
6475
|
-
[cols.cacheReadTokens,
|
|
6476
|
-
[cols.cacheWriteTokens,
|
|
6477
|
-
[cols.costUsd,
|
|
6951
|
+
[cols.tokensIn, run3.tokensIn],
|
|
6952
|
+
[cols.tokensOut, run3.tokensOut],
|
|
6953
|
+
[cols.cacheReadTokens, run3.cacheReadTokens],
|
|
6954
|
+
[cols.cacheWriteTokens, run3.cacheWriteTokens],
|
|
6955
|
+
[cols.costUsd, run3.costUsd]
|
|
6478
6956
|
];
|
|
6479
6957
|
const out = {};
|
|
6480
6958
|
for (const [column, value2] of figures) {
|
|
@@ -6497,41 +6975,41 @@ async function post(target, path, body) {
|
|
|
6497
6975
|
if (!res.ok) throw new AgentLogHttpError(`${path}: ${res.status} ${res.statusText}`, res.status);
|
|
6498
6976
|
return await res.json();
|
|
6499
6977
|
}
|
|
6500
|
-
async function reportAgentRun(target,
|
|
6978
|
+
async function reportAgentRun(target, run3) {
|
|
6501
6979
|
let ticketRecordId;
|
|
6502
|
-
if (target.tickets &&
|
|
6980
|
+
if (target.tickets && run3.ticketReference) {
|
|
6503
6981
|
try {
|
|
6504
|
-
ticketRecordId = await findTicketRecord(target, target.tickets,
|
|
6982
|
+
ticketRecordId = await findTicketRecord(target, target.tickets, run3.ticketReference);
|
|
6505
6983
|
} catch {
|
|
6506
6984
|
}
|
|
6507
6985
|
}
|
|
6508
6986
|
const body = {
|
|
6509
6987
|
agentId: target.agentId,
|
|
6510
|
-
ticketReference:
|
|
6511
|
-
outcome:
|
|
6512
|
-
startedAt:
|
|
6513
|
-
finishedAt:
|
|
6988
|
+
ticketReference: run3.ticketReference,
|
|
6989
|
+
outcome: run3.outcome,
|
|
6990
|
+
startedAt: run3.startedAt,
|
|
6991
|
+
finishedAt: run3.finishedAt,
|
|
6514
6992
|
source: "client",
|
|
6515
6993
|
client: "crew",
|
|
6516
|
-
model:
|
|
6517
|
-
tokensIn:
|
|
6518
|
-
tokensOut:
|
|
6519
|
-
cacheReadTokens:
|
|
6520
|
-
cacheWriteTokens:
|
|
6521
|
-
costUsd:
|
|
6522
|
-
numTurns:
|
|
6523
|
-
prefixTokens:
|
|
6524
|
-
promptVersion:
|
|
6525
|
-
promptSha:
|
|
6994
|
+
model: run3.model,
|
|
6995
|
+
tokensIn: run3.tokensIn,
|
|
6996
|
+
tokensOut: run3.tokensOut,
|
|
6997
|
+
cacheReadTokens: run3.cacheReadTokens,
|
|
6998
|
+
cacheWriteTokens: run3.cacheWriteTokens,
|
|
6999
|
+
costUsd: run3.costUsd,
|
|
7000
|
+
numTurns: run3.numTurns,
|
|
7001
|
+
prefixTokens: run3.prefixTokens,
|
|
7002
|
+
promptVersion: run3.promptVersion,
|
|
7003
|
+
promptSha: run3.promptSha
|
|
6526
7004
|
};
|
|
6527
7005
|
const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
|
|
6528
7006
|
let entry;
|
|
6529
7007
|
try {
|
|
6530
|
-
entry = await post(target, "/agents/log", { ...body, ...workItem, harness:
|
|
7008
|
+
entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run3.harness, provider: run3.provider });
|
|
6531
7009
|
} catch (e) {
|
|
6532
7010
|
if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
|
|
6533
7011
|
if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
|
|
6534
|
-
if (e.status !== 400 || !
|
|
7012
|
+
if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId) {
|
|
6535
7013
|
e.row = true;
|
|
6536
7014
|
throw e;
|
|
6537
7015
|
}
|
|
@@ -6542,7 +7020,7 @@ async function reportAgentRun(target, run2) {
|
|
|
6542
7020
|
throw e2;
|
|
6543
7021
|
}
|
|
6544
7022
|
}
|
|
6545
|
-
for (const cycle of
|
|
7023
|
+
for (const cycle of run3.cycles) {
|
|
6546
7024
|
await post(target, `/agents/log/${entry.id}/cycles`, {
|
|
6547
7025
|
cycleIndex: cycle.cycleIndex,
|
|
6548
7026
|
occurredAt: cycle.occurredAt,
|
|
@@ -6550,7 +7028,7 @@ async function reportAgentRun(target, run2) {
|
|
|
6550
7028
|
...cycle.role ? { role: cycle.role } : {}
|
|
6551
7029
|
});
|
|
6552
7030
|
}
|
|
6553
|
-
const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId,
|
|
7031
|
+
const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
|
|
6554
7032
|
return {
|
|
6555
7033
|
agentLogId: entry.id,
|
|
6556
7034
|
...counters?.warning ? { counterWarning: counters.warning } : {},
|
|
@@ -6564,38 +7042,38 @@ async function writeCounters(target, w) {
|
|
|
6564
7042
|
if (Object.keys(body).length === 0) return;
|
|
6565
7043
|
await request(target, "PATCH", `/data-models/${w.issuesModelId}/records/${w.recordId}`, body);
|
|
6566
7044
|
}
|
|
6567
|
-
async function addRunToTicket(target, t, recordId,
|
|
7045
|
+
async function addRunToTicket(target, t, recordId, run3) {
|
|
6568
7046
|
const write = {
|
|
6569
7047
|
issuesModelId: t.issuesModelId,
|
|
6570
7048
|
recordId,
|
|
6571
|
-
ticketReference:
|
|
7049
|
+
ticketReference: run3.ticketReference,
|
|
6572
7050
|
counters: t.counters,
|
|
6573
|
-
body: counterIncrements(
|
|
7051
|
+
body: counterIncrements(run3, t.counters, new Set(Object.values(t.counters)))
|
|
6574
7052
|
};
|
|
6575
7053
|
try {
|
|
6576
7054
|
await writeCounters(target, write);
|
|
6577
7055
|
return {};
|
|
6578
7056
|
} catch (e) {
|
|
6579
7057
|
return {
|
|
6580
|
-
warning: `could not add run spend to ${
|
|
7058
|
+
warning: `could not add run spend to ${run3.ticketReference}: ${e.message}`,
|
|
6581
7059
|
...isRetryable(e) ? { retry: write } : {}
|
|
6582
7060
|
};
|
|
6583
7061
|
}
|
|
6584
7062
|
}
|
|
6585
|
-
var queueDir = (stateDir) =>
|
|
7063
|
+
var queueDir = (stateDir) => join17(stateDir, "pending-agent-logs");
|
|
6586
7064
|
function queueAgentRun(stateDir, name, q) {
|
|
6587
|
-
|
|
6588
|
-
|
|
7065
|
+
mkdirSync11(queueDir(stateDir), { recursive: true });
|
|
7066
|
+
writeFileSync12(join17(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6589
7067
|
}
|
|
6590
7068
|
async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6591
7069
|
const dir = queueDir(stateDir);
|
|
6592
7070
|
if (!existsSync15(dir)) return 0;
|
|
6593
7071
|
let sent = 0;
|
|
6594
7072
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
6595
|
-
const path =
|
|
7073
|
+
const path = join17(dir, f);
|
|
6596
7074
|
let q;
|
|
6597
7075
|
try {
|
|
6598
|
-
q = JSON.parse(
|
|
7076
|
+
q = JSON.parse(readFileSync16(path, "utf8"));
|
|
6599
7077
|
} catch {
|
|
6600
7078
|
continue;
|
|
6601
7079
|
}
|
|
@@ -6614,17 +7092,17 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
6614
7092
|
attempts: 0,
|
|
6615
7093
|
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6616
7094
|
});
|
|
6617
|
-
|
|
7095
|
+
rmSync11(path, { force: true });
|
|
6618
7096
|
sent += 1;
|
|
6619
7097
|
log.emit(`sent a queued Agent Log report (${f})`);
|
|
6620
7098
|
} catch (e) {
|
|
6621
7099
|
if (!(e instanceof AgentLogUnreachableError)) {
|
|
6622
|
-
|
|
7100
|
+
rmSync11(path, { force: true });
|
|
6623
7101
|
log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
|
|
6624
7102
|
continue;
|
|
6625
7103
|
}
|
|
6626
7104
|
const attempts = q.attempts + 1;
|
|
6627
|
-
|
|
7105
|
+
writeFileSync12(path, JSON.stringify({
|
|
6628
7106
|
...q,
|
|
6629
7107
|
attempts,
|
|
6630
7108
|
warned: true,
|
|
@@ -6635,20 +7113,20 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
6635
7113
|
}
|
|
6636
7114
|
return sent;
|
|
6637
7115
|
}
|
|
6638
|
-
var countersDir = (stateDir) =>
|
|
7116
|
+
var countersDir = (stateDir) => join17(stateDir, "pending-ticket-counters");
|
|
6639
7117
|
function queueTicketCounters(stateDir, name, q) {
|
|
6640
|
-
|
|
6641
|
-
|
|
7118
|
+
mkdirSync11(countersDir(stateDir), { recursive: true });
|
|
7119
|
+
writeFileSync12(join17(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6642
7120
|
}
|
|
6643
7121
|
async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6644
7122
|
const dir = countersDir(stateDir);
|
|
6645
7123
|
if (!existsSync15(dir)) return 0;
|
|
6646
7124
|
let sent = 0;
|
|
6647
7125
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
6648
|
-
const path =
|
|
7126
|
+
const path = join17(dir, f);
|
|
6649
7127
|
let q;
|
|
6650
7128
|
try {
|
|
6651
|
-
q = JSON.parse(
|
|
7129
|
+
q = JSON.parse(readFileSync16(path, "utf8"));
|
|
6652
7130
|
} catch {
|
|
6653
7131
|
continue;
|
|
6654
7132
|
}
|
|
@@ -6657,17 +7135,17 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
6657
7135
|
const apiKey = await keyFor(q.role);
|
|
6658
7136
|
if (!apiKey) continue;
|
|
6659
7137
|
await writeCounters({ ...q.target, apiKey }, q.write);
|
|
6660
|
-
|
|
7138
|
+
rmSync11(path, { force: true });
|
|
6661
7139
|
sent += 1;
|
|
6662
7140
|
log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
|
|
6663
7141
|
} catch (e) {
|
|
6664
7142
|
if (!isRetryable(e)) {
|
|
6665
|
-
|
|
7143
|
+
rmSync11(path, { force: true });
|
|
6666
7144
|
log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
|
|
6667
7145
|
continue;
|
|
6668
7146
|
}
|
|
6669
7147
|
const attempts = q.attempts + 1;
|
|
6670
|
-
|
|
7148
|
+
writeFileSync12(path, JSON.stringify({
|
|
6671
7149
|
...q,
|
|
6672
7150
|
attempts,
|
|
6673
7151
|
warned: true,
|
|
@@ -6870,8 +7348,8 @@ var HandoffTranscriptTracker = class {
|
|
|
6870
7348
|
};
|
|
6871
7349
|
|
|
6872
7350
|
// src/pending-handoff.ts
|
|
6873
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
6874
|
-
import { join as
|
|
7351
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, readdirSync as readdirSync7, readFileSync as readFileSync17, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "fs";
|
|
7352
|
+
import { join as join20 } from "path";
|
|
6875
7353
|
|
|
6876
7354
|
// src/ticket-branch.ts
|
|
6877
7355
|
import { basename as basename6 } from "path";
|
|
@@ -8038,15 +8516,15 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
8038
8516
|
|
|
8039
8517
|
// src/run-worktree.ts
|
|
8040
8518
|
import { existsSync as existsSync17 } from "fs";
|
|
8041
|
-
import { dirname as dirname7, join as
|
|
8519
|
+
import { dirname as dirname7, join as join19 } from "path";
|
|
8042
8520
|
|
|
8043
8521
|
// src/ports.ts
|
|
8044
|
-
import { execFileSync as
|
|
8522
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
8045
8523
|
import { existsSync as existsSync16 } from "fs";
|
|
8046
|
-
import { join as
|
|
8047
|
-
var
|
|
8524
|
+
import { join as join18 } from "path";
|
|
8525
|
+
var run2 = (cmd, args) => {
|
|
8048
8526
|
try {
|
|
8049
|
-
return
|
|
8527
|
+
return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8050
8528
|
} catch {
|
|
8051
8529
|
return "";
|
|
8052
8530
|
}
|
|
@@ -8060,7 +8538,7 @@ function listeners() {
|
|
|
8060
8538
|
seen.add(key);
|
|
8061
8539
|
out.push({ pid, port });
|
|
8062
8540
|
};
|
|
8063
|
-
const lsof =
|
|
8541
|
+
const lsof = run2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
|
|
8064
8542
|
if (lsof) {
|
|
8065
8543
|
for (const line of lsof.split("\n").slice(1)) {
|
|
8066
8544
|
const cols = line.split(/\s+/);
|
|
@@ -8070,7 +8548,7 @@ function listeners() {
|
|
|
8070
8548
|
}
|
|
8071
8549
|
return out;
|
|
8072
8550
|
}
|
|
8073
|
-
const ss =
|
|
8551
|
+
const ss = run2("ss", ["-ltnpH"]);
|
|
8074
8552
|
for (const line of ss.split("\n")) {
|
|
8075
8553
|
const local = line.trim().split(/\s+/)[3];
|
|
8076
8554
|
const pid = /pid=(\d+)/.exec(line)?.[1];
|
|
@@ -8088,7 +8566,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
8088
8566
|
}
|
|
8089
8567
|
function worktreeExists(parent, prefix, n) {
|
|
8090
8568
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
8091
|
-
if (existsSync16(
|
|
8569
|
+
if (existsSync16(join18(parent, name, ".git"))) return true;
|
|
8092
8570
|
}
|
|
8093
8571
|
return false;
|
|
8094
8572
|
}
|
|
@@ -8122,7 +8600,7 @@ async function killGently(pids, graceMs = 2e3) {
|
|
|
8122
8600
|
function pidsInWorktree(path, n, s = DEFAULT_PORTS) {
|
|
8123
8601
|
const pids = /* @__PURE__ */ new Set();
|
|
8124
8602
|
for (const port of [s.backendBase + n, s.frontendBase + n]) for (const p of pidsOnPort(port)) pids.add(p);
|
|
8125
|
-
const matched =
|
|
8603
|
+
const matched = run2("pgrep", ["-f", `${path}/`]);
|
|
8126
8604
|
for (const line of matched.split("\n")) {
|
|
8127
8605
|
const pid = Number(line.trim());
|
|
8128
8606
|
if (Number.isFinite(pid) && pid > 0 && pid !== process.pid) pids.add(pid);
|
|
@@ -8187,7 +8665,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
8187
8665
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
8188
8666
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
8189
8667
|
}
|
|
8190
|
-
const path =
|
|
8668
|
+
const path = join19(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
8191
8669
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
8192
8670
|
return { action: "cut-remote", path, branch: located.branch };
|
|
8193
8671
|
}
|
|
@@ -8644,12 +9122,12 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
|
|
|
8644
9122
|
|
|
8645
9123
|
// src/pending-handoff.ts
|
|
8646
9124
|
var PENDING_MAX_AGE_MS = 48 * 36e5;
|
|
8647
|
-
var pendingDirFor = (stateDir) =>
|
|
8648
|
-
var stateDirOfStream = (streamPath) =>
|
|
9125
|
+
var pendingDirFor = (stateDir) => join20(stateDir, "pending-handoffs");
|
|
9126
|
+
var stateDirOfStream = (streamPath) => join20(streamPath, "..", "..");
|
|
8649
9127
|
function writePendingHandoff(stateDir, rec) {
|
|
8650
9128
|
const dir = pendingDirFor(stateDir);
|
|
8651
|
-
|
|
8652
|
-
|
|
9129
|
+
mkdirSync12(dir, { recursive: true });
|
|
9130
|
+
writeFileSync13(join20(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
8653
9131
|
}
|
|
8654
9132
|
function listPendingHandoffs(stateDir, route2) {
|
|
8655
9133
|
const dir = pendingDirFor(stateDir);
|
|
@@ -8657,7 +9135,7 @@ function listPendingHandoffs(stateDir, route2) {
|
|
|
8657
9135
|
const out = [];
|
|
8658
9136
|
for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
8659
9137
|
try {
|
|
8660
|
-
const rec = JSON.parse(
|
|
9138
|
+
const rec = JSON.parse(readFileSync17(join20(dir, f), "utf8"));
|
|
8661
9139
|
if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
|
|
8662
9140
|
} catch {
|
|
8663
9141
|
}
|
|
@@ -8665,7 +9143,7 @@ function listPendingHandoffs(stateDir, route2) {
|
|
|
8665
9143
|
return out;
|
|
8666
9144
|
}
|
|
8667
9145
|
function removePendingHandoff(stateDir, ticketRecordId) {
|
|
8668
|
-
|
|
9146
|
+
rmSync12(join20(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
|
|
8669
9147
|
}
|
|
8670
9148
|
function pendingHandoffFor(stateDir, ticketRecordId) {
|
|
8671
9149
|
return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
|
|
@@ -8775,8 +9253,8 @@ function expandSkillMarkers(text2, lookup2, opts = {}) {
|
|
|
8775
9253
|
}
|
|
8776
9254
|
|
|
8777
9255
|
// src/skills.ts
|
|
8778
|
-
import { readFileSync as
|
|
8779
|
-
import { join as
|
|
9256
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync8, existsSync as existsSync20 } from "fs";
|
|
9257
|
+
import { join as join22 } from "path";
|
|
8780
9258
|
import { parse as parseYaml } from "yaml";
|
|
8781
9259
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
8782
9260
|
|
|
@@ -8792,8 +9270,8 @@ ${additions}`;
|
|
|
8792
9270
|
}
|
|
8793
9271
|
|
|
8794
9272
|
// src/agents.ts
|
|
8795
|
-
import { readFileSync as
|
|
8796
|
-
import { join as
|
|
9273
|
+
import { readFileSync as readFileSync18, existsSync as existsSync19 } from "fs";
|
|
9274
|
+
import { join as join21 } from "path";
|
|
8797
9275
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
|
|
8798
9276
|
var PERSONA_NAME = {
|
|
8799
9277
|
dev: "Developer",
|
|
@@ -8807,12 +9285,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8807
9285
|
var AgentsSyncError = class extends Error {
|
|
8808
9286
|
};
|
|
8809
9287
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8810
|
-
const briefPath =
|
|
9288
|
+
const briefPath = join21(promptsDir, "personas", `lane-${role}.md`);
|
|
8811
9289
|
if (!existsSync19(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8812
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
8813
|
-
const commonPath =
|
|
8814
|
-
if (!existsSync19(commonPath)) return
|
|
8815
|
-
return `${
|
|
9290
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync18(briefPath, "utf8");
|
|
9291
|
+
const commonPath = join21(promptsDir, "personas", "common.md");
|
|
9292
|
+
if (!existsSync19(commonPath)) return readFileSync18(briefPath, "utf8");
|
|
9293
|
+
return `${readFileSync18(commonPath, "utf8")}${readFileSync18(briefPath, "utf8")}`;
|
|
8816
9294
|
}
|
|
8817
9295
|
async function resolveAgentId(route2, role, opts) {
|
|
8818
9296
|
const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -9259,7 +9737,7 @@ function describeCrewLink(o) {
|
|
|
9259
9737
|
var SkillSyncError = class extends Error {
|
|
9260
9738
|
};
|
|
9261
9739
|
function parseSkillFile(path) {
|
|
9262
|
-
const text2 =
|
|
9740
|
+
const text2 = readFileSync19(path, "utf8");
|
|
9263
9741
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text2);
|
|
9264
9742
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
9265
9743
|
const frontmatter = match[1];
|
|
@@ -9272,9 +9750,9 @@ function parseSkillFile(path) {
|
|
|
9272
9750
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
9273
9751
|
}
|
|
9274
9752
|
function listLocalSkills(promptsDir) {
|
|
9275
|
-
const dir =
|
|
9753
|
+
const dir = join22(promptsDir, "skills");
|
|
9276
9754
|
if (!existsSync20(dir)) return [];
|
|
9277
|
-
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9755
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join22(dir, f)));
|
|
9278
9756
|
}
|
|
9279
9757
|
async function syncSkills(route2, opts) {
|
|
9280
9758
|
if (!route2.resolved) {
|
|
@@ -9457,6 +9935,13 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
|
9457
9935
|
return out;
|
|
9458
9936
|
}
|
|
9459
9937
|
|
|
9938
|
+
// src/runner.ts
|
|
9939
|
+
import { spawn as spawn3, execFile, execFileSync as execFileSync6 } from "child_process";
|
|
9940
|
+
import { mkdtempSync, writeFileSync as writeFileSync14, rmSync as rmSync13 } from "fs";
|
|
9941
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
9942
|
+
import { basename as basename7, join as join23 } from "path";
|
|
9943
|
+
import { promisify } from "util";
|
|
9944
|
+
|
|
9460
9945
|
// src/run-limit.ts
|
|
9461
9946
|
var DEFAULT_MAX_RUN_MINUTES = 240;
|
|
9462
9947
|
var KILL_GRACE_MS = 5e3;
|
|
@@ -9506,6 +9991,186 @@ function runKilledComment(b) {
|
|
|
9506
9991
|
The agent run on this ticket was stopped at its wall-clock limit of ${b.limitMinutes} minutes before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The worktree is left as it was. The next run on this seat resumes from it rather than starting over.`;
|
|
9507
9992
|
}
|
|
9508
9993
|
|
|
9994
|
+
// src/runner.ts
|
|
9995
|
+
var execFileP = promisify(execFile);
|
|
9996
|
+
var DEFAULT_SANDBOX_CONFIG = {
|
|
9997
|
+
bin: "openshell",
|
|
9998
|
+
mirrorDir: "/cache",
|
|
9999
|
+
workDir: "/sandbox/work"
|
|
10000
|
+
};
|
|
10001
|
+
var SANDBOX_STDIN_LIMIT = 4 * 1024 * 1024 - 64 * 1024;
|
|
10002
|
+
var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
|
|
10003
|
+
var HostRunner = class {
|
|
10004
|
+
kind = "host";
|
|
10005
|
+
start(plan, o) {
|
|
10006
|
+
const child = spawn3(plan.bin, plan.args, {
|
|
10007
|
+
cwd: plan.cwd,
|
|
10008
|
+
env: o.env,
|
|
10009
|
+
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10010
|
+
detached: true
|
|
10011
|
+
});
|
|
10012
|
+
return { child, handle: { runner: "host", pid: child.pid } };
|
|
10013
|
+
}
|
|
10014
|
+
async kill(handle) {
|
|
10015
|
+
if (handle.pid !== void 0) await killRunGroup(handle.pid);
|
|
10016
|
+
}
|
|
10017
|
+
isAlive(handle) {
|
|
10018
|
+
if (handle.pid === void 0) return false;
|
|
10019
|
+
try {
|
|
10020
|
+
process.kill(handle.pid, 0);
|
|
10021
|
+
return true;
|
|
10022
|
+
} catch (e) {
|
|
10023
|
+
return e.code === "EPERM";
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
reattach(meta) {
|
|
10027
|
+
return { runner: "host", pid: meta.pid };
|
|
10028
|
+
}
|
|
10029
|
+
async release() {
|
|
10030
|
+
}
|
|
10031
|
+
};
|
|
10032
|
+
function shQuote2(s) {
|
|
10033
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10034
|
+
}
|
|
10035
|
+
function sandboxName(plan, now = Date.now()) {
|
|
10036
|
+
const base = [plan.route, plan.role, plan.ticket ?? ""].join("-").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10037
|
+
return `crew-${base.slice(0, 36)}-${now.toString(36)}`.replace(/-+/g, "-");
|
|
10038
|
+
}
|
|
10039
|
+
var SandboxRunner = class {
|
|
10040
|
+
kind = "sandbox";
|
|
10041
|
+
cfg;
|
|
10042
|
+
/** Injected for tests; the real one reads the plan's checkout. */
|
|
10043
|
+
originOf;
|
|
10044
|
+
constructor(cfg = {}, originOf = gitOrigin) {
|
|
10045
|
+
this.cfg = { ...DEFAULT_SANDBOX_CONFIG, ...cfg };
|
|
10046
|
+
this.originOf = originOf;
|
|
10047
|
+
}
|
|
10048
|
+
os(args, o = {}) {
|
|
10049
|
+
return execFileP(this.cfg.bin, args, { env: o.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
|
|
10050
|
+
}
|
|
10051
|
+
async start(plan, o) {
|
|
10052
|
+
const origin = this.originOf(plan.cwd);
|
|
10053
|
+
if (!origin) throw new Error(`cannot find the origin remote of ${plan.cwd}, so there is nothing to clone into the sandbox`);
|
|
10054
|
+
const repoName = basename7(origin).replace(/\.git$/, "");
|
|
10055
|
+
const name = sandboxName(plan);
|
|
10056
|
+
const { cfg } = this;
|
|
10057
|
+
const secretKeys = Object.keys(plan.setEnv);
|
|
10058
|
+
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0 && !secretKeys.includes(cfg.forgeTokenVar)) secretKeys.push(cfg.forgeTokenVar);
|
|
10059
|
+
const createEnv = { ...process.env, ...plan.setEnv };
|
|
10060
|
+
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0) createEnv[cfg.forgeTokenVar] = o.env[cfg.forgeTokenVar];
|
|
10061
|
+
const create2 = ["sandbox", "create", "--name", name, "--detach"];
|
|
10062
|
+
if (cfg.policy) create2.push("--policy", cfg.policy);
|
|
10063
|
+
if (cfg.image) create2.push("--from", cfg.image);
|
|
10064
|
+
if (cfg.cacheVolume) {
|
|
10065
|
+
create2.push("--driver-config-json", JSON.stringify({
|
|
10066
|
+
docker: { mounts: [{ type: "volume", source: cfg.cacheVolume, target: cfg.mirrorDir, read_only: true }] }
|
|
10067
|
+
}));
|
|
10068
|
+
}
|
|
10069
|
+
for (const k of secretKeys) create2.push("--env-from", k);
|
|
10070
|
+
const maxSeconds = Math.max(60, Math.ceil((plan.maxRunMinutes ?? 0) * 60) + 600);
|
|
10071
|
+
create2.push("--", "sleep", String(plan.maxRunMinutes ? maxSeconds : 86400));
|
|
10072
|
+
const t0 = Date.now();
|
|
10073
|
+
try {
|
|
10074
|
+
await this.os(create2, { env: createEnv });
|
|
10075
|
+
} catch (e) {
|
|
10076
|
+
await this.deleteQuietly(name);
|
|
10077
|
+
throw new Error(`openshell could not create the sandbox: ${describeExecError(e)}`);
|
|
10078
|
+
}
|
|
10079
|
+
o.emit.emit(`sandbox_created: ${name}`, { role: plan.role, data: { event: "sandbox_created", sandbox: name, ms: Date.now() - t0 } });
|
|
10080
|
+
const handle = { runner: "sandbox", sandbox: name, sandboxBin: cfg.bin };
|
|
10081
|
+
try {
|
|
10082
|
+
const branch = `crew-run-${name.slice(-8)}`;
|
|
10083
|
+
const clone = [
|
|
10084
|
+
`git clone --quiet --shared ${shQuote2(`${cfg.mirrorDir}/${repoName}.git`)} ${shQuote2(cfg.workDir)}`,
|
|
10085
|
+
`cd ${shQuote2(cfg.workDir)}`,
|
|
10086
|
+
`git remote set-url origin ${shQuote2(origin)}`,
|
|
10087
|
+
`git checkout --quiet -b ${shQuote2(branch)}`
|
|
10088
|
+
].join(" && ");
|
|
10089
|
+
await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
|
|
10090
|
+
const harness = cfg.agentBin ?? basename7(plan.bin);
|
|
10091
|
+
const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
|
|
10092
|
+
if (viaUpload) {
|
|
10093
|
+
const tmp = mkdtempSync(join23(tmpdir3(), "crew-prompt-"));
|
|
10094
|
+
try {
|
|
10095
|
+
const file = join23(tmp, "prompt.txt");
|
|
10096
|
+
writeFileSync14(file, plan.prompt, { mode: 384 });
|
|
10097
|
+
await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
|
|
10098
|
+
} finally {
|
|
10099
|
+
rmSync13(tmp, { recursive: true, force: true });
|
|
10100
|
+
}
|
|
10101
|
+
}
|
|
10102
|
+
const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
|
|
10103
|
+
const child = spawn3(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
|
|
10104
|
+
env: process.env,
|
|
10105
|
+
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10106
|
+
detached: true
|
|
10107
|
+
});
|
|
10108
|
+
return { child, handle: { ...handle, pid: child.pid }, ...viaUpload ? { promptDelivered: true } : {} };
|
|
10109
|
+
} catch (e) {
|
|
10110
|
+
await this.release(handle, o.emit);
|
|
10111
|
+
throw new Error(`could not prepare the sandbox ${name}: ${describeExecError(e)}`);
|
|
10112
|
+
}
|
|
10113
|
+
}
|
|
10114
|
+
async kill(handle) {
|
|
10115
|
+
if (handle.sandbox) await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
|
|
10116
|
+
if (handle.pid !== void 0) await killRunGroup(handle.pid, { graceMs: 2e3 });
|
|
10117
|
+
}
|
|
10118
|
+
isAlive(handle) {
|
|
10119
|
+
return new HostRunner().isAlive(handle);
|
|
10120
|
+
}
|
|
10121
|
+
reattach(meta) {
|
|
10122
|
+
return { runner: "sandbox", pid: meta.pid, sandbox: meta.sandbox, sandboxBin: meta.sandboxBin };
|
|
10123
|
+
}
|
|
10124
|
+
async release(handle, emit) {
|
|
10125
|
+
if (!handle.sandbox) return;
|
|
10126
|
+
const t0 = Date.now();
|
|
10127
|
+
await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
|
|
10128
|
+
emit.emit(`sandbox_deleted: ${handle.sandbox}`, { data: { event: "sandbox_deleted", sandbox: handle.sandbox, ms: Date.now() - t0 } });
|
|
10129
|
+
}
|
|
10130
|
+
async deleteQuietly(name, bin = this.cfg.bin) {
|
|
10131
|
+
try {
|
|
10132
|
+
await execFileP(bin, ["sandbox", "delete", name], { timeout: 6e4 });
|
|
10133
|
+
} catch {
|
|
10134
|
+
}
|
|
10135
|
+
}
|
|
10136
|
+
};
|
|
10137
|
+
function describeExecError(e) {
|
|
10138
|
+
const err3 = e;
|
|
10139
|
+
return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
|
|
10140
|
+
}
|
|
10141
|
+
function gitOrigin(cwd) {
|
|
10142
|
+
try {
|
|
10143
|
+
return execFileSync6("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
10144
|
+
} catch {
|
|
10145
|
+
return null;
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
function runnerFor(kind, sandbox) {
|
|
10149
|
+
return kind === "sandbox" ? new SandboxRunner(sandbox) : new HostRunner();
|
|
10150
|
+
}
|
|
10151
|
+
function runnerOfSidecar(sc) {
|
|
10152
|
+
return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
|
|
10153
|
+
}
|
|
10154
|
+
function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync6(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
|
|
10155
|
+
const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
|
|
10156
|
+
let version;
|
|
10157
|
+
try {
|
|
10158
|
+
version = run3(bin, ["--version"]).trim();
|
|
10159
|
+
} catch (e) {
|
|
10160
|
+
return { ok: false, detail: `${bin} is not installed or not on the scheduler PATH (${e.message.split("\n")[0]})` };
|
|
10161
|
+
}
|
|
10162
|
+
try {
|
|
10163
|
+
run3(bin, ["status"]);
|
|
10164
|
+
} catch (e) {
|
|
10165
|
+
const err3 = e;
|
|
10166
|
+
return {
|
|
10167
|
+
ok: false,
|
|
10168
|
+
detail: `${version}: the gateway does not answer (${(err3.stderr || err3.message).trim().split("\n").filter(Boolean).slice(0, 2).join(" / ")}). Start it; on Docker Desktop its docker section needs supervisor_network_mode = "bridge", and allow_driver_config = true for the cache volume mount.`
|
|
10169
|
+
};
|
|
10170
|
+
}
|
|
10171
|
+
return { ok: true, detail: version };
|
|
10172
|
+
}
|
|
10173
|
+
|
|
9509
10174
|
// src/agent.ts
|
|
9510
10175
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
9511
10176
|
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
@@ -9533,7 +10198,7 @@ var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
|
|
|
9533
10198
|
function streamShowsAllowanceRefusal(streamPath) {
|
|
9534
10199
|
let raw;
|
|
9535
10200
|
try {
|
|
9536
|
-
raw =
|
|
10201
|
+
raw = readFileSync20(streamPath, "utf8");
|
|
9537
10202
|
} catch {
|
|
9538
10203
|
return false;
|
|
9539
10204
|
}
|
|
@@ -9556,13 +10221,13 @@ function streamShowsAllowanceRefusal(streamPath) {
|
|
|
9556
10221
|
}
|
|
9557
10222
|
return false;
|
|
9558
10223
|
}
|
|
9559
|
-
var digestPathFor = (stateDir, routeLabel, role, runId) =>
|
|
10224
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join24(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
9560
10225
|
function planAgentRun(o) {
|
|
9561
|
-
const briefPath =
|
|
10226
|
+
const briefPath = join24(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
9562
10227
|
if (!existsSync21(briefPath)) {
|
|
9563
10228
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
9564
10229
|
}
|
|
9565
|
-
const commonPath =
|
|
10230
|
+
const commonPath = join24(o.route.promptsDir, "personas", "common.md");
|
|
9566
10231
|
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync21(commonPath);
|
|
9567
10232
|
const now = o.now ?? (() => Date.now());
|
|
9568
10233
|
const routeLabel = routeSlug(o.route.route);
|
|
@@ -9572,10 +10237,10 @@ function planAgentRun(o) {
|
|
|
9572
10237
|
let ageSeconds;
|
|
9573
10238
|
if (existsSync21(digestPath2)) {
|
|
9574
10239
|
ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
|
|
9575
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
10240
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync20(digestPath2, "utf8");
|
|
9576
10241
|
}
|
|
9577
|
-
const common = usesCommon ?
|
|
9578
|
-
const brief =
|
|
10242
|
+
const common = usesCommon ? readFileSync20(commonPath, "utf8") : "";
|
|
10243
|
+
const brief = readFileSync20(briefPath, "utf8");
|
|
9579
10244
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
9580
10245
|
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
9581
10246
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -9596,8 +10261,8 @@ function planAgentRun(o) {
|
|
|
9596
10261
|
brief: expanded.text,
|
|
9597
10262
|
digest
|
|
9598
10263
|
});
|
|
9599
|
-
const promptSha =
|
|
9600
|
-
const streamsDir =
|
|
10264
|
+
const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
|
|
10265
|
+
const streamsDir = join24(o.stateDir, "streams");
|
|
9601
10266
|
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
9602
10267
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
9603
10268
|
const { model } = seat;
|
|
@@ -9658,6 +10323,7 @@ function planAgentRun(o) {
|
|
|
9658
10323
|
workspaceAuth: toolchain.auth === "workspace",
|
|
9659
10324
|
provider: toolchainProvider(toolchain),
|
|
9660
10325
|
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
10326
|
+
...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
|
|
9661
10327
|
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
9662
10328
|
warnings: [...seat.warnings ?? [], ...expanded.warnings],
|
|
9663
10329
|
prompt,
|
|
@@ -9682,8 +10348,8 @@ function planAgentRun(o) {
|
|
|
9682
10348
|
digestAttached: digest !== void 0,
|
|
9683
10349
|
digestAgeSeconds: ageSeconds,
|
|
9684
10350
|
model,
|
|
9685
|
-
streamPath:
|
|
9686
|
-
eventsPath:
|
|
10351
|
+
streamPath: join24(streamsDir, `${base}.jsonl`),
|
|
10352
|
+
eventsPath: join24(streamsDir, `${base}.events.jsonl`),
|
|
9687
10353
|
ticket: o.ticket,
|
|
9688
10354
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
9689
10355
|
repo: o.repo ?? null,
|
|
@@ -9714,7 +10380,7 @@ function describePlan(p) {
|
|
|
9714
10380
|
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
9715
10381
|
function readRunSidecar(streamPath) {
|
|
9716
10382
|
try {
|
|
9717
|
-
const sc = JSON.parse(
|
|
10383
|
+
const sc = JSON.parse(readFileSync20(pidPathFor(streamPath), "utf8"));
|
|
9718
10384
|
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
9719
10385
|
} catch {
|
|
9720
10386
|
return null;
|
|
@@ -9747,7 +10413,7 @@ function identityOf(plan, adapterHarness) {
|
|
|
9747
10413
|
promptSha: plan.promptSha
|
|
9748
10414
|
};
|
|
9749
10415
|
}
|
|
9750
|
-
async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes) {
|
|
10416
|
+
async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
|
|
9751
10417
|
const elapsedMs = Date.now() - Date.parse(spawnedAt);
|
|
9752
10418
|
const limit = limitMinutes ?? Math.round(elapsedMs / 6e4);
|
|
9753
10419
|
emit.emit(`run_killed: ${role} run exceeded its wall-clock limit of ${limit} min (ran ${Math.round(elapsedMs / 6e4)} min)`, {
|
|
@@ -9755,13 +10421,13 @@ async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes)
|
|
|
9755
10421
|
...ticket ? { ticket } : {},
|
|
9756
10422
|
data: { event: "run_killed", reason: WALL_CLOCK_REASON, limitMinutes: limit, elapsedMs, pid }
|
|
9757
10423
|
});
|
|
9758
|
-
await
|
|
10424
|
+
await kill();
|
|
9759
10425
|
return { limitMinutes: limit, elapsedMs };
|
|
9760
10426
|
}
|
|
9761
10427
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
9762
10428
|
function openSink(path) {
|
|
9763
10429
|
try {
|
|
9764
|
-
|
|
10430
|
+
mkdirSync13(dirname8(path), { recursive: true });
|
|
9765
10431
|
return createWriteStream(path, { flags: "a" });
|
|
9766
10432
|
} catch {
|
|
9767
10433
|
return void 0;
|
|
@@ -9943,7 +10609,7 @@ var RunObserver = class {
|
|
|
9943
10609
|
};
|
|
9944
10610
|
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
9945
10611
|
try {
|
|
9946
|
-
|
|
10612
|
+
writeFileSync15(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
9947
10613
|
route: id.route,
|
|
9948
10614
|
role: id.role,
|
|
9949
10615
|
hint: id.ticket,
|
|
@@ -10050,7 +10716,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
10050
10716
|
if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
|
|
10051
10717
|
if (sent.counterRetry) {
|
|
10052
10718
|
try {
|
|
10053
|
-
queueTicketCounters(stateDirOfStream(id.streamPath),
|
|
10719
|
+
queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
10054
10720
|
version: 1,
|
|
10055
10721
|
route: id.route,
|
|
10056
10722
|
role: id.role,
|
|
@@ -10065,7 +10731,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
10065
10731
|
} catch (e) {
|
|
10066
10732
|
if (e instanceof AgentLogUnreachableError) {
|
|
10067
10733
|
try {
|
|
10068
|
-
queueAgentRun(stateDirOfStream(id.streamPath),
|
|
10734
|
+
queueAgentRun(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
10069
10735
|
version: 1,
|
|
10070
10736
|
route: id.route,
|
|
10071
10737
|
role: id.role,
|
|
@@ -10095,102 +10761,125 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
10095
10761
|
}
|
|
10096
10762
|
function spawnAgent(plan, emit, hintMoved) {
|
|
10097
10763
|
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
10764
|
+
const runner = runnerFor(plan.runner, plan.sandboxConfig);
|
|
10098
10765
|
const started = Date.now();
|
|
10099
10766
|
const startedAt = new Date(started).toISOString();
|
|
10100
10767
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
10101
10768
|
const eventsSink = openSink(plan.eventsPath);
|
|
10102
10769
|
const id = identityOf(plan, adapter.harness);
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10770
|
+
let streamFd;
|
|
10771
|
+
try {
|
|
10772
|
+
mkdirSync13(dirname8(plan.streamPath), { recursive: true });
|
|
10773
|
+
streamFd = openSync3(plan.streamPath, "a");
|
|
10774
|
+
} catch (e) {
|
|
10775
|
+
void endSink(eventsSink);
|
|
10776
|
+
return Promise.reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
|
|
10777
|
+
}
|
|
10778
|
+
const failed = (e) => {
|
|
10779
|
+
void endSink(eventsSink);
|
|
10780
|
+
return Promise.reject(new AgentSpawnError(`cannot run ${plan.bin}: ${e.message}`));
|
|
10781
|
+
};
|
|
10782
|
+
let begun;
|
|
10783
|
+
try {
|
|
10784
|
+
begun = runner.start(plan, { env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv }, streamFd, emit });
|
|
10785
|
+
} catch (e) {
|
|
10119
10786
|
closeSync3(streamFd);
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
ticketRecordId: plan.ticketRecordId ?? null,
|
|
10132
|
-
repo: plan.repo ?? null,
|
|
10133
|
-
routeName: plan.routeName,
|
|
10134
|
-
model: plan.model,
|
|
10135
|
-
toolchain: id.toolchain,
|
|
10136
|
-
harness: id.harness,
|
|
10137
|
-
provider: id.provider,
|
|
10138
|
-
eventsPath: plan.eventsPath,
|
|
10139
|
-
agentLog: id.agentLog,
|
|
10140
|
-
promptVersion: plan.promptVersion,
|
|
10141
|
-
promptSha: plan.promptSha
|
|
10142
|
-
};
|
|
10143
|
-
try {
|
|
10144
|
-
writeFileSync13(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
10145
|
-
} catch {
|
|
10787
|
+
return failed(e);
|
|
10788
|
+
}
|
|
10789
|
+
if (begun instanceof Promise) {
|
|
10790
|
+
return begun.then(
|
|
10791
|
+
(run3) => {
|
|
10792
|
+
closeSync3(streamFd);
|
|
10793
|
+
return supervise(run3);
|
|
10794
|
+
},
|
|
10795
|
+
(e) => {
|
|
10796
|
+
closeSync3(streamFd);
|
|
10797
|
+
return failed(e);
|
|
10146
10798
|
}
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10799
|
+
);
|
|
10800
|
+
}
|
|
10801
|
+
closeSync3(streamFd);
|
|
10802
|
+
return supervise(begun);
|
|
10803
|
+
function supervise({ child, handle, promptDelivered }) {
|
|
10804
|
+
return new Promise((resolve7, reject) => {
|
|
10805
|
+
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10806
|
+
const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
|
|
10807
|
+
if (child.pid !== void 0) {
|
|
10808
|
+
const sidecar = {
|
|
10809
|
+
pid: child.pid,
|
|
10810
|
+
ownerPid: process.pid,
|
|
10811
|
+
spawnedAt,
|
|
10812
|
+
role: plan.role,
|
|
10813
|
+
route: plan.route,
|
|
10814
|
+
...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
|
|
10815
|
+
...handle.runner === "sandbox" ? { runner: "sandbox", sandbox: handle.sandbox, sandboxBin: handle.sandboxBin } : {},
|
|
10816
|
+
ticket: plan.ticket ?? null,
|
|
10817
|
+
ticketRecordId: plan.ticketRecordId ?? null,
|
|
10818
|
+
repo: plan.repo ?? null,
|
|
10819
|
+
routeName: plan.routeName,
|
|
10820
|
+
model: plan.model,
|
|
10821
|
+
toolchain: id.toolchain,
|
|
10822
|
+
harness: id.harness,
|
|
10823
|
+
provider: id.provider,
|
|
10824
|
+
eventsPath: plan.eventsPath,
|
|
10825
|
+
agentLog: id.agentLog,
|
|
10826
|
+
promptVersion: plan.promptVersion,
|
|
10827
|
+
promptSha: plan.promptSha
|
|
10828
|
+
};
|
|
10829
|
+
try {
|
|
10830
|
+
writeFileSync15(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
10831
|
+
} catch {
|
|
10832
|
+
}
|
|
10177
10833
|
}
|
|
10178
|
-
const
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10834
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
10835
|
+
const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
|
|
10836
|
+
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
10837
|
+
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
10838
|
+
const tail3 = new FileTail(plan.streamPath);
|
|
10839
|
+
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
10840
|
+
let killed;
|
|
10841
|
+
const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
|
|
10842
|
+
void killForDeadline(() => runner.kill(handle, WALL_CLOCK_REASON), child.pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
|
|
10843
|
+
killed = k;
|
|
10844
|
+
});
|
|
10845
|
+
}, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
|
|
10846
|
+
let spawnFailed = false;
|
|
10847
|
+
child.on("error", (err3) => {
|
|
10848
|
+
spawnFailed = true;
|
|
10849
|
+
clearInterval(timer);
|
|
10850
|
+
clearTimeout(limitTimer);
|
|
10851
|
+
void runner.release(handle, emit);
|
|
10852
|
+
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
10853
|
+
});
|
|
10854
|
+
child.on("close", async (code) => {
|
|
10855
|
+
clearInterval(timer);
|
|
10856
|
+
clearTimeout(limitTimer);
|
|
10857
|
+
obs.feed(tail3.poll());
|
|
10858
|
+
obs.end();
|
|
10859
|
+
await endSink(eventsSink);
|
|
10860
|
+
if (spawnFailed) return;
|
|
10861
|
+
await runner.release(handle, emit);
|
|
10862
|
+
if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
|
|
10863
|
+
killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
|
|
10864
|
+
}
|
|
10865
|
+
const { ms: ms3, handoffPending } = await finishRun(
|
|
10866
|
+
id,
|
|
10867
|
+
obs,
|
|
10868
|
+
emit,
|
|
10869
|
+
startedAt,
|
|
10870
|
+
{ code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
|
|
10871
|
+
plan.agentLog?.apiKey,
|
|
10872
|
+
hintMoved
|
|
10873
|
+
);
|
|
10874
|
+
resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
10875
|
+
});
|
|
10876
|
+
child.stdin?.on("error", (err3) => {
|
|
10877
|
+
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
10878
|
+
});
|
|
10879
|
+
if (promptDelivered) child.stdin?.end();
|
|
10880
|
+
else child.stdin?.end(plan.prompt);
|
|
10191
10881
|
});
|
|
10192
|
-
|
|
10193
|
-
});
|
|
10882
|
+
}
|
|
10194
10883
|
}
|
|
10195
10884
|
async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
10196
10885
|
const adapter = adapterFor(sc.harness);
|
|
@@ -10202,6 +10891,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
10202
10891
|
obs.muted = true;
|
|
10203
10892
|
obs.feed(tail3.poll());
|
|
10204
10893
|
obs.muted = false;
|
|
10894
|
+
const runner = runnerOfSidecar(sc);
|
|
10205
10895
|
const adopted = isAlive(sc);
|
|
10206
10896
|
let killed;
|
|
10207
10897
|
if (adopted) {
|
|
@@ -10210,7 +10900,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
10210
10900
|
while (isAlive(sc)) {
|
|
10211
10901
|
obs.feed(tail3.poll());
|
|
10212
10902
|
if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
|
|
10213
|
-
killed = await killForDeadline(sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
|
|
10903
|
+
killed = await killForDeadline(() => runner.kill(runner.reattach(sc), WALL_CLOCK_REASON), sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
|
|
10214
10904
|
continue;
|
|
10215
10905
|
}
|
|
10216
10906
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
@@ -10219,6 +10909,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
10219
10909
|
obs.feed(tail3.poll());
|
|
10220
10910
|
obs.end();
|
|
10221
10911
|
await endSink(eventsSink);
|
|
10912
|
+
await runner.release(runner.reattach(sc), emit);
|
|
10222
10913
|
const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
10223
10914
|
const { ms: ms3, ticket } = await finishRun(
|
|
10224
10915
|
id,
|
|
@@ -10349,9 +11040,9 @@ async function mintMissingSeatKeys(route2, o) {
|
|
|
10349
11040
|
out.push({ role, action: "no_agent" });
|
|
10350
11041
|
continue;
|
|
10351
11042
|
}
|
|
10352
|
-
const
|
|
11043
|
+
const slug2 = slugFor(route2, agentId);
|
|
10353
11044
|
try {
|
|
10354
|
-
if ((await store.get(host,
|
|
11045
|
+
if ((await store.get(host, slug2))?.apiKey) {
|
|
10355
11046
|
out.push({ role, action: "already_stored" });
|
|
10356
11047
|
continue;
|
|
10357
11048
|
}
|
|
@@ -10389,7 +11080,7 @@ async function mintMissingSeatKeys(route2, o) {
|
|
|
10389
11080
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10390
11081
|
...body.id ? { keyId: body.id } : {}
|
|
10391
11082
|
};
|
|
10392
|
-
await store.set(host,
|
|
11083
|
+
await store.set(host, slug2, session);
|
|
10393
11084
|
} catch (e) {
|
|
10394
11085
|
let revoked = false;
|
|
10395
11086
|
if (body.id) {
|
|
@@ -10453,9 +11144,9 @@ async function revokeSeatKeys(route2, o) {
|
|
|
10453
11144
|
out.push({ role, action: "no_agent" });
|
|
10454
11145
|
continue;
|
|
10455
11146
|
}
|
|
10456
|
-
const
|
|
11147
|
+
const slug2 = slugFor(route2, agentId);
|
|
10457
11148
|
try {
|
|
10458
|
-
const session = await store.get(host,
|
|
11149
|
+
const session = await store.get(host, slug2);
|
|
10459
11150
|
if (!session?.apiKey) {
|
|
10460
11151
|
out.push({ role, action: "not_stored" });
|
|
10461
11152
|
continue;
|
|
@@ -10476,7 +11167,7 @@ async function revokeSeatKeys(route2, o) {
|
|
|
10476
11167
|
out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
|
|
10477
11168
|
continue;
|
|
10478
11169
|
}
|
|
10479
|
-
await store.clear(host,
|
|
11170
|
+
await store.clear(host, slug2);
|
|
10480
11171
|
out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
|
|
10481
11172
|
} catch (e) {
|
|
10482
11173
|
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
@@ -10528,7 +11219,7 @@ var realDeps = {
|
|
|
10528
11219
|
resolve7(r);
|
|
10529
11220
|
}
|
|
10530
11221
|
};
|
|
10531
|
-
const child =
|
|
11222
|
+
const child = spawn5(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
10532
11223
|
const killGroup = () => {
|
|
10533
11224
|
try {
|
|
10534
11225
|
if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
|
|
@@ -10626,17 +11317,17 @@ var REMEDY = {
|
|
|
10626
11317
|
timeout: "the harness did not answer in time \u2014 run it by hand to see where it stalls",
|
|
10627
11318
|
"wrong-answer": "the agent answered but did not name the workspace \u2014 run it by hand to see what it said"
|
|
10628
11319
|
};
|
|
10629
|
-
function classify(spec,
|
|
11320
|
+
function classify(spec, run3, seen) {
|
|
10630
11321
|
const bin = spec.launcher ?? spec.bin;
|
|
10631
11322
|
const fail = (failure, what) => ({ failure, detail: `${what} \u2014 ${REMEDY[failure]}` });
|
|
10632
|
-
if (
|
|
10633
|
-
if (
|
|
10634
|
-
return fail("binary-not-found", `could not start \`${bin}\` (${
|
|
11323
|
+
if (run3.spawnError) {
|
|
11324
|
+
if (run3.spawnError.code === "ENOENT") return fail("binary-not-found", `\`${bin}\` is not on the scheduler PATH`);
|
|
11325
|
+
return fail("binary-not-found", `could not start \`${bin}\` (${run3.spawnError.message})`);
|
|
10635
11326
|
}
|
|
10636
|
-
if (
|
|
11327
|
+
if (run3.timedOut) return fail("timeout", "no answer within the time limit");
|
|
10637
11328
|
const text2 = `${seen.toolOutput}
|
|
10638
|
-
${
|
|
10639
|
-
if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(
|
|
11329
|
+
${run3.stderr}`;
|
|
11330
|
+
if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run3.stderr)) {
|
|
10640
11331
|
return fail("key-refused", "the tracker returned 401/403 to the MCP call");
|
|
10641
11332
|
}
|
|
10642
11333
|
if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text2}
|
|
@@ -10644,8 +11335,8 @@ ${seen.texts.join("\n")}`)) {
|
|
|
10644
11335
|
return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
|
|
10645
11336
|
}
|
|
10646
11337
|
const answered = seen.texts.length > 0 || seen.finalText;
|
|
10647
|
-
if (!answered || LOGIN.test(
|
|
10648
|
-
return fail("login-missing", `\`${bin}\` exited${
|
|
11338
|
+
if (!answered || LOGIN.test(run3.stderr) || LOGIN.test(seen.finalText ?? "")) {
|
|
11339
|
+
return fail("login-missing", `\`${bin}\` exited${run3.code !== null ? ` (${run3.code})` : ""} before answering; the vendor login is probably missing`);
|
|
10649
11340
|
}
|
|
10650
11341
|
return fail("wrong-answer", `the agent replied "${(seen.finalText ?? seen.texts.at(-1) ?? "").trim().slice(0, 80)}"`);
|
|
10651
11342
|
}
|
|
@@ -10673,9 +11364,9 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
10673
11364
|
...adapter.runEnv(spec),
|
|
10674
11365
|
[KEY_VAR]: i.apiKey
|
|
10675
11366
|
};
|
|
10676
|
-
const cwd =
|
|
11367
|
+
const cwd = mkdtempSync2(join25(tmpdir4(), "crew-selftest-"));
|
|
10677
11368
|
try {
|
|
10678
|
-
const
|
|
11369
|
+
const run3 = await deps.run({
|
|
10679
11370
|
bin: invocation.bin,
|
|
10680
11371
|
args: invocation.args,
|
|
10681
11372
|
cwd,
|
|
@@ -10684,18 +11375,18 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
10684
11375
|
`,
|
|
10685
11376
|
timeoutMs: i.timeoutMs ?? SELFTEST_TIMEOUT_MS
|
|
10686
11377
|
});
|
|
10687
|
-
const seen = observe(spec,
|
|
11378
|
+
const seen = observe(spec, run3.stdout);
|
|
10688
11379
|
const answer = [seen.finalText ?? "", ...seen.texts].join("\n");
|
|
10689
11380
|
const usage2 = { ...seen.tokens !== void 0 ? { tokens: seen.tokens } : {}, ...seen.costUsd !== void 0 ? { costUsd: seen.costUsd } : {} };
|
|
10690
|
-
if (!
|
|
11381
|
+
if (!run3.spawnError && !run3.timedOut && answer.toLowerCase().includes(i.workspaceName.toLowerCase())) {
|
|
10691
11382
|
const auth = spec.harness === "claude" ? ` on ${describeClaudeAuth(spec)}` : "";
|
|
10692
11383
|
return finish({ ok: true, workspace: i.workspaceName, detail: `read workspace "${i.workspaceName}"${auth}`, ...usage2 });
|
|
10693
11384
|
}
|
|
10694
|
-
const { failure, detail } = classify(spec,
|
|
11385
|
+
const { failure, detail } = classify(spec, run3, seen);
|
|
10695
11386
|
return finish({ ok: false, failure, detail, ...usage2 });
|
|
10696
11387
|
} finally {
|
|
10697
11388
|
try {
|
|
10698
|
-
|
|
11389
|
+
rmSync14(cwd, { recursive: true, force: true });
|
|
10699
11390
|
} catch {
|
|
10700
11391
|
}
|
|
10701
11392
|
}
|
|
@@ -10706,11 +11397,11 @@ function formatResult(r) {
|
|
|
10706
11397
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
10707
11398
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
10708
11399
|
}
|
|
10709
|
-
var fileFor2 = (stateDir) =>
|
|
11400
|
+
var fileFor2 = (stateDir) => join25(stateDir, "selftest.json");
|
|
10710
11401
|
function readSelftest(stateDir) {
|
|
10711
11402
|
try {
|
|
10712
11403
|
const p = fileFor2(stateDir);
|
|
10713
|
-
return existsSync22(p) ? JSON.parse(
|
|
11404
|
+
return existsSync22(p) ? JSON.parse(readFileSync21(p, "utf8")) : {};
|
|
10714
11405
|
} catch {
|
|
10715
11406
|
return {};
|
|
10716
11407
|
}
|
|
@@ -10718,8 +11409,8 @@ function readSelftest(stateDir) {
|
|
|
10718
11409
|
function recordSelftest(stateDir, results) {
|
|
10719
11410
|
const all = readSelftest(stateDir);
|
|
10720
11411
|
for (const r of results) all[r.toolchain] = r;
|
|
10721
|
-
|
|
10722
|
-
|
|
11412
|
+
mkdirSync14(stateDir, { recursive: true });
|
|
11413
|
+
writeFileSync16(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
|
|
10723
11414
|
`);
|
|
10724
11415
|
}
|
|
10725
11416
|
function doctorSelftestLines(stored, toolchains) {
|
|
@@ -10731,7 +11422,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10731
11422
|
}
|
|
10732
11423
|
|
|
10733
11424
|
// src/cli.ts
|
|
10734
|
-
import { resolve as resolve6, dirname as dirname25, join as
|
|
11425
|
+
import { resolve as resolve6, dirname as dirname25, join as join49, basename as basename15 } from "path";
|
|
10735
11426
|
import { fileURLToPath } from "url";
|
|
10736
11427
|
import { hostname as hostname4, homedir as homedir11 } from "os";
|
|
10737
11428
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
@@ -10928,14 +11619,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
10928
11619
|
}
|
|
10929
11620
|
|
|
10930
11621
|
// src/connect-resume.ts
|
|
10931
|
-
import { mkdirSync as
|
|
10932
|
-
import { dirname as dirname9, join as
|
|
11622
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync22, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
|
|
11623
|
+
import { dirname as dirname9, join as join26 } from "path";
|
|
10933
11624
|
function lastSessionFile(stateDir) {
|
|
10934
|
-
return
|
|
11625
|
+
return join26(stateDir, "connect-session.json");
|
|
10935
11626
|
}
|
|
10936
11627
|
function readPointers(stateDir) {
|
|
10937
11628
|
try {
|
|
10938
|
-
const parsed = JSON.parse(
|
|
11629
|
+
const parsed = JSON.parse(readFileSync22(lastSessionFile(stateDir), "utf8"));
|
|
10939
11630
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
10940
11631
|
const out = {};
|
|
10941
11632
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -10949,8 +11640,8 @@ function readPointers(stateDir) {
|
|
|
10949
11640
|
}
|
|
10950
11641
|
function writePointers(stateDir, pointers) {
|
|
10951
11642
|
const file = lastSessionFile(stateDir);
|
|
10952
|
-
|
|
10953
|
-
|
|
11643
|
+
mkdirSync15(dirname9(file), { recursive: true });
|
|
11644
|
+
writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
|
|
10954
11645
|
`, { mode: 384 });
|
|
10955
11646
|
}
|
|
10956
11647
|
function rememberSession(stateDir, host, workspaceSlug) {
|
|
@@ -10967,7 +11658,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
|
|
|
10967
11658
|
const { [host]: _old, ...rest } = all;
|
|
10968
11659
|
const next = left.length ? { ...rest, [host]: left } : rest;
|
|
10969
11660
|
if (Object.keys(next).length) writePointers(stateDir, next);
|
|
10970
|
-
else
|
|
11661
|
+
else rmSync15(lastSessionFile(stateDir), { force: true });
|
|
10971
11662
|
} catch {
|
|
10972
11663
|
}
|
|
10973
11664
|
}
|
|
@@ -11007,7 +11698,7 @@ function forgetSession(stateDir, host) {
|
|
|
11007
11698
|
try {
|
|
11008
11699
|
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
11009
11700
|
if (Object.keys(rest).length) writePointers(stateDir, rest);
|
|
11010
|
-
else
|
|
11701
|
+
else rmSync15(lastSessionFile(stateDir), { force: true });
|
|
11011
11702
|
} catch {
|
|
11012
11703
|
}
|
|
11013
11704
|
}
|
|
@@ -11035,15 +11726,15 @@ async function signOutSessions(o) {
|
|
|
11035
11726
|
}
|
|
11036
11727
|
|
|
11037
11728
|
// src/reset.ts
|
|
11038
|
-
import { existsSync as existsSync23, mkdirSync as
|
|
11039
|
-
import { dirname as dirname10, join as
|
|
11729
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync9, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
|
|
11730
|
+
import { dirname as dirname10, join as join27 } from "path";
|
|
11040
11731
|
import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
11041
11732
|
function clonedCheckoutsFile(stateDir) {
|
|
11042
|
-
return
|
|
11733
|
+
return join27(stateDir, "cloned-checkouts.json");
|
|
11043
11734
|
}
|
|
11044
11735
|
function readClonedCheckouts(stateDir) {
|
|
11045
11736
|
try {
|
|
11046
|
-
const parsed = JSON.parse(
|
|
11737
|
+
const parsed = JSON.parse(readFileSync23(clonedCheckoutsFile(stateDir), "utf8"));
|
|
11047
11738
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
11048
11739
|
} catch {
|
|
11049
11740
|
return [];
|
|
@@ -11053,8 +11744,8 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
11053
11744
|
const known = readClonedCheckouts(stateDir);
|
|
11054
11745
|
if (known.includes(dir)) return;
|
|
11055
11746
|
const file = clonedCheckoutsFile(stateDir);
|
|
11056
|
-
|
|
11057
|
-
|
|
11747
|
+
mkdirSync16(dirname10(file), { recursive: true });
|
|
11748
|
+
writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
11058
11749
|
`);
|
|
11059
11750
|
}
|
|
11060
11751
|
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
@@ -11109,7 +11800,7 @@ async function runReset(o, fx) {
|
|
|
11109
11800
|
if (o.beforeState) steps.push(...await o.beforeState());
|
|
11110
11801
|
steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
|
|
11111
11802
|
if (existsSync23(o.configFile)) {
|
|
11112
|
-
if (!o.dryRun)
|
|
11803
|
+
if (!o.dryRun) rmSync16(o.configFile, { force: true });
|
|
11113
11804
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
11114
11805
|
} else {
|
|
11115
11806
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -11125,21 +11816,21 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
11125
11816
|
if (linkedWorktrees.length) {
|
|
11126
11817
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
11127
11818
|
}
|
|
11128
|
-
if (!dryRun)
|
|
11819
|
+
if (!dryRun) rmSync16(dir, { recursive: true, force: true });
|
|
11129
11820
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
11130
11821
|
}
|
|
11131
11822
|
function removeState(stateDir, dryRun) {
|
|
11132
11823
|
const out = [];
|
|
11133
|
-
const targets = [
|
|
11824
|
+
const targets = [join27(stateDir, "resolved"), join27(stateDir, "keys"), clonedCheckoutsFile(stateDir), join27(stateDir, "connect-session.json"), join27(stateDir, "reload-requested"), join27(stateDir, "daemon.started")];
|
|
11134
11825
|
try {
|
|
11135
11826
|
for (const name of readdirSync9(stateDir)) {
|
|
11136
|
-
if (name.startsWith(".") && statSync8(
|
|
11827
|
+
if (name.startsWith(".") && statSync8(join27(stateDir, name)).isFile()) targets.push(join27(stateDir, name));
|
|
11137
11828
|
}
|
|
11138
11829
|
} catch {
|
|
11139
11830
|
}
|
|
11140
11831
|
for (const t of targets) {
|
|
11141
11832
|
if (!existsSync23(t)) continue;
|
|
11142
|
-
if (!dryRun)
|
|
11833
|
+
if (!dryRun) rmSync16(t, { recursive: true, force: true });
|
|
11143
11834
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
11144
11835
|
}
|
|
11145
11836
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -11153,7 +11844,7 @@ function wipeStateDir(stateDir, dryRun) {
|
|
|
11153
11844
|
return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
|
|
11154
11845
|
}
|
|
11155
11846
|
if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
|
|
11156
|
-
if (!dryRun) for (const n of names)
|
|
11847
|
+
if (!dryRun) for (const n of names) rmSync16(join27(stateDir, n), { recursive: true, force: true });
|
|
11157
11848
|
return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
|
|
11158
11849
|
}
|
|
11159
11850
|
function describeResetStep(s) {
|
|
@@ -11177,21 +11868,21 @@ async function runDeprovision(o, fx, deps) {
|
|
|
11177
11868
|
};
|
|
11178
11869
|
const afterScheduler = async () => {
|
|
11179
11870
|
const steps2 = [];
|
|
11180
|
-
for (const
|
|
11181
|
-
const target = `${
|
|
11871
|
+
for (const run3 of deps.liveRuns()) {
|
|
11872
|
+
const target = `${run3.role} run on ${run3.route} (pid ${run3.pid})`;
|
|
11182
11873
|
if (o.dryRun) {
|
|
11183
11874
|
steps2.push({ step: "run", target, action: "would-remove" });
|
|
11184
11875
|
continue;
|
|
11185
11876
|
}
|
|
11186
11877
|
try {
|
|
11187
|
-
await deps.killRun(
|
|
11188
|
-
deps.recordKill(
|
|
11878
|
+
await deps.killRun(run3);
|
|
11879
|
+
deps.recordKill(run3);
|
|
11189
11880
|
} catch (e) {
|
|
11190
11881
|
steps2.push({ step: "run", target, action: "failed", detail: e.message });
|
|
11191
11882
|
continue;
|
|
11192
11883
|
}
|
|
11193
11884
|
try {
|
|
11194
|
-
await deps.handBack(
|
|
11885
|
+
await deps.handBack(run3);
|
|
11195
11886
|
steps2.push({ step: "run", target, action: "removed", detail: "killed" });
|
|
11196
11887
|
} catch (e) {
|
|
11197
11888
|
steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
|
|
@@ -11238,8 +11929,8 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
11238
11929
|
import { existsSync as existsSync26, readdirSync as readdirSync12 } from "fs";
|
|
11239
11930
|
|
|
11240
11931
|
// src/interrupted.ts
|
|
11241
|
-
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as
|
|
11242
|
-
import { join as
|
|
11932
|
+
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
|
|
11933
|
+
import { join as join28 } from "path";
|
|
11243
11934
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
11244
11935
|
function interruptedComment(b) {
|
|
11245
11936
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -11260,19 +11951,19 @@ function runFinished(streamPath) {
|
|
|
11260
11951
|
const path = `${streamPath}.meta.json`;
|
|
11261
11952
|
if (!existsSync24(path)) return false;
|
|
11262
11953
|
try {
|
|
11263
|
-
return JSON.parse(
|
|
11954
|
+
return JSON.parse(readFileSync24(path, "utf8")).running !== true;
|
|
11264
11955
|
} catch {
|
|
11265
11956
|
return true;
|
|
11266
11957
|
}
|
|
11267
11958
|
}
|
|
11268
11959
|
function orphanedStreams(o) {
|
|
11269
|
-
const dir =
|
|
11960
|
+
const dir = join28(o.stateDir, "streams");
|
|
11270
11961
|
if (!existsSync24(dir)) return [];
|
|
11271
11962
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
11272
11963
|
const out = [];
|
|
11273
11964
|
for (const name of readdirSync10(dir)) {
|
|
11274
11965
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
11275
|
-
const streamPath =
|
|
11966
|
+
const streamPath = join28(dir, name);
|
|
11276
11967
|
if (runFinished(streamPath)) continue;
|
|
11277
11968
|
const sidecar = readRunSidecar(streamPath);
|
|
11278
11969
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -11319,283 +12010,79 @@ async function reconcileRuns(o) {
|
|
|
11319
12010
|
}
|
|
11320
12011
|
} else {
|
|
11321
12012
|
await work;
|
|
11322
|
-
}
|
|
11323
|
-
}
|
|
11324
|
-
return { adopted, interrupted };
|
|
11325
|
-
}
|
|
11326
|
-
|
|
11327
|
-
// src/live-runs.ts
|
|
11328
|
-
import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
|
|
11329
|
-
import { join as join27 } from "path";
|
|
11330
|
-
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
11331
|
-
const dir = join27(stateDir, "streams");
|
|
11332
|
-
if (!existsSync25(dir)) return [];
|
|
11333
|
-
const out = [];
|
|
11334
|
-
for (const name of readdirSync11(dir)) {
|
|
11335
|
-
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
11336
|
-
const streamPath = join27(dir, name);
|
|
11337
|
-
if (runFinished(streamPath)) continue;
|
|
11338
|
-
const sidecar = readRunSidecar(streamPath);
|
|
11339
|
-
if (!sidecar || !isAlive(sidecar)) continue;
|
|
11340
|
-
out.push({ ...sidecar, streamPath });
|
|
11341
|
-
}
|
|
11342
|
-
return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
|
|
11343
|
-
}
|
|
11344
|
-
function siblingRuns(runs, ownTicketRecordId) {
|
|
11345
|
-
return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
|
|
11346
|
-
}
|
|
11347
|
-
async function settleRunEnd(o) {
|
|
11348
|
-
const { tracker, emit } = o;
|
|
11349
|
-
const all = o.runs ?? liveRuns(o.stateDir);
|
|
11350
|
-
const slug = routeSlug(o.route.route);
|
|
11351
|
-
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug), o.ownTicketRecordId);
|
|
11352
|
-
const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
|
|
11353
|
-
const newest = (rs) => rs[rs.length - 1];
|
|
11354
|
-
if (o.memberId) {
|
|
11355
|
-
try {
|
|
11356
|
-
if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
|
|
11357
|
-
else await tracker.setCrewStatus(o.memberId, "idle");
|
|
11358
|
-
} catch (e) {
|
|
11359
|
-
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
11360
|
-
}
|
|
11361
|
-
}
|
|
11362
|
-
try {
|
|
11363
|
-
if (shipSiblings.length) {
|
|
11364
|
-
const n = newest(shipSiblings);
|
|
11365
|
-
await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
|
|
11366
|
-
} else {
|
|
11367
|
-
await tracker.beatIdle(o.shipName);
|
|
11368
|
-
}
|
|
11369
|
-
} catch (e) {
|
|
11370
|
-
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
11371
|
-
}
|
|
11372
|
-
}
|
|
11373
|
-
function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
|
|
11374
|
-
try {
|
|
11375
|
-
return worktreeForNumber(d, n)?.path ?? null;
|
|
11376
|
-
} catch {
|
|
11377
|
-
return null;
|
|
11378
|
-
}
|
|
11379
|
-
}) {
|
|
11380
|
-
const ownRoute = run2.route === routeSlug(route2.route);
|
|
11381
|
-
const repoDir = run2.repo ? reposOf(route2).find((t) => t.name === run2.repo)?.dir : void 0;
|
|
11382
|
-
const num = run2.ticket?.replace(/^\D+/, "") ?? "";
|
|
11383
|
-
return {
|
|
11384
|
-
role: run2.role,
|
|
11385
|
-
seat: ownRoute ? seatIds[run2.role] ?? null : null,
|
|
11386
|
-
ticket: run2.ticket,
|
|
11387
|
-
ticketRecordId: run2.ticketRecordId,
|
|
11388
|
-
repo: run2.repo ?? null,
|
|
11389
|
-
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
11390
|
-
stream: run2.streamPath,
|
|
11391
|
-
pid: run2.pid,
|
|
11392
|
-
startedAt: run2.spawnedAt,
|
|
11393
|
-
deadline: run2.deadline ?? null
|
|
11394
|
-
};
|
|
11395
|
-
}
|
|
11396
|
-
|
|
11397
|
-
// src/passenger-containers.ts
|
|
11398
|
-
import { execFileSync as execFileSync5 } from "child_process";
|
|
11399
|
-
import { createHash as createHash5 } from "crypto";
|
|
11400
|
-
var realExec = (cmd, args, opts) => {
|
|
11401
|
-
try {
|
|
11402
|
-
return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
11403
|
-
} catch (e) {
|
|
11404
|
-
const err3 = e;
|
|
11405
|
-
const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
11406
|
-
throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
11407
|
-
}
|
|
11408
|
-
};
|
|
11409
|
-
var DOCKER_INFO_TIMEOUT_MS = 1e4;
|
|
11410
|
-
function dockerAvailable(exec = realExec) {
|
|
11411
|
-
try {
|
|
11412
|
-
exec("docker", ["info"], { timeoutMs: DOCKER_INFO_TIMEOUT_MS });
|
|
11413
|
-
return true;
|
|
11414
|
-
} catch {
|
|
11415
|
-
return false;
|
|
11416
|
-
}
|
|
11417
|
-
}
|
|
11418
|
-
var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
|
|
11419
|
-
function passengerImageRef(cfg) {
|
|
11420
|
-
return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
|
|
11421
|
-
}
|
|
11422
|
-
function imagePresent(image, exec = realExec) {
|
|
11423
|
-
try {
|
|
11424
|
-
exec("docker", ["image", "inspect", image]);
|
|
11425
|
-
return true;
|
|
11426
|
-
} catch {
|
|
11427
|
-
return false;
|
|
12013
|
+
}
|
|
11428
12014
|
}
|
|
12015
|
+
return { adopted, interrupted };
|
|
11429
12016
|
}
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
}
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
const
|
|
11438
|
-
|
|
12017
|
+
|
|
12018
|
+
// src/live-runs.ts
|
|
12019
|
+
import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
|
|
12020
|
+
import { join as join29 } from "path";
|
|
12021
|
+
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
12022
|
+
const dir = join29(stateDir, "streams");
|
|
12023
|
+
if (!existsSync25(dir)) return [];
|
|
12024
|
+
const out = [];
|
|
12025
|
+
for (const name of readdirSync11(dir)) {
|
|
12026
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
12027
|
+
const streamPath = join29(dir, name);
|
|
12028
|
+
if (runFinished(streamPath)) continue;
|
|
12029
|
+
const sidecar = readRunSidecar(streamPath);
|
|
12030
|
+
if (!sidecar || !isAlive(sidecar)) continue;
|
|
12031
|
+
out.push({ ...sidecar, streamPath });
|
|
12032
|
+
}
|
|
12033
|
+
return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
|
|
11439
12034
|
}
|
|
11440
|
-
function
|
|
11441
|
-
|
|
11442
|
-
return createHash5("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
12035
|
+
function siblingRuns(runs, ownTicketRecordId) {
|
|
12036
|
+
return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
|
|
11443
12037
|
}
|
|
11444
|
-
function
|
|
11445
|
-
const
|
|
11446
|
-
const
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
12038
|
+
async function settleRunEnd(o) {
|
|
12039
|
+
const { tracker, emit } = o;
|
|
12040
|
+
const all = o.runs ?? liveRuns(o.stateDir);
|
|
12041
|
+
const slug2 = routeSlug(o.route.route);
|
|
12042
|
+
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug2), o.ownTicketRecordId);
|
|
12043
|
+
const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
|
|
12044
|
+
const newest = (rs) => rs[rs.length - 1];
|
|
12045
|
+
if (o.memberId) {
|
|
12046
|
+
try {
|
|
12047
|
+
if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
|
|
12048
|
+
else await tracker.setCrewStatus(o.memberId, "idle");
|
|
12049
|
+
} catch (e) {
|
|
12050
|
+
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
11453
12051
|
}
|
|
11454
|
-
byWorkspace.set(workspaceId, mounts);
|
|
11455
12052
|
}
|
|
11456
|
-
return [...byWorkspace.entries()].filter(([, mounts]) => mounts.size > 0).map(([workspaceId, mounts]) => {
|
|
11457
|
-
const mountList = [...mounts.values()];
|
|
11458
|
-
return {
|
|
11459
|
-
workspaceId,
|
|
11460
|
-
containerName: containerNameFor(workspaceId),
|
|
11461
|
-
port: portFor(workspaceId),
|
|
11462
|
-
image,
|
|
11463
|
-
mounts: mountList,
|
|
11464
|
-
mountsHash: mountsDigest(mountList, image)
|
|
11465
|
-
};
|
|
11466
|
-
});
|
|
11467
|
-
}
|
|
11468
|
-
function listPassengerContainers(exec = realExec) {
|
|
11469
|
-
const out = exec("docker", ["ps", "-a", "--filter", "name=crew-passenger-", "--format", "{{.Names}} {{.State}}"]);
|
|
11470
|
-
return out.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
|
|
11471
|
-
const [name, state] = line.split(" ");
|
|
11472
|
-
return { name, running: state === "running" };
|
|
11473
|
-
});
|
|
11474
|
-
}
|
|
11475
|
-
function currentMountsHash(name, exec) {
|
|
11476
12053
|
try {
|
|
11477
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
12054
|
+
if (shipSiblings.length) {
|
|
12055
|
+
const n = newest(shipSiblings);
|
|
12056
|
+
await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
|
|
12057
|
+
} else {
|
|
12058
|
+
await tracker.beatIdle(o.shipName);
|
|
12059
|
+
}
|
|
12060
|
+
} catch (e) {
|
|
12061
|
+
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
11482
12062
|
}
|
|
11483
12063
|
}
|
|
11484
|
-
function
|
|
12064
|
+
function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
|
|
11485
12065
|
try {
|
|
11486
|
-
|
|
12066
|
+
return worktreeForNumber(d, n)?.path ?? null;
|
|
11487
12067
|
} catch {
|
|
12068
|
+
return null;
|
|
11488
12069
|
}
|
|
11489
|
-
}
|
|
11490
|
-
|
|
11491
|
-
const
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
"--label",
|
|
11505
|
-
`crew.mounts.hash=${plan.mountsHash}`
|
|
11506
|
-
];
|
|
11507
|
-
if (sync) {
|
|
11508
|
-
args.push(
|
|
11509
|
-
"--add-host",
|
|
11510
|
-
"host.docker.internal:host-gateway",
|
|
11511
|
-
"-e",
|
|
11512
|
-
`PASSENGER_SYNC_URL=http://${sync.host}:${sync.port}/sync`,
|
|
11513
|
-
"-e",
|
|
11514
|
-
`PASSENGER_SYNC_SECRET=${sync.secret}`,
|
|
11515
|
-
"-e",
|
|
11516
|
-
`PASSENGER_MCP_SECRET=${sync.secret}`
|
|
11517
|
-
);
|
|
11518
|
-
}
|
|
11519
|
-
for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
|
|
11520
|
-
args.push(plan.image);
|
|
11521
|
-
exec("docker", args);
|
|
11522
|
-
}
|
|
11523
|
-
function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
|
|
11524
|
-
const plans = planContainers(cfg);
|
|
11525
|
-
const plannedByName = new Map(plans.map((p) => [p.containerName, p]));
|
|
11526
|
-
const existing = listPassengerContainers(exec);
|
|
11527
|
-
const existingByName = new Map(existing.map((c) => [c.name, c]));
|
|
11528
|
-
const started = [];
|
|
11529
|
-
const recreated = [];
|
|
11530
|
-
const stopped = [];
|
|
11531
|
-
const ensured = /* @__PURE__ */ new Set();
|
|
11532
|
-
const start = (plan, sync) => {
|
|
11533
|
-
if (!ensured.has(plan.image)) {
|
|
11534
|
-
ensureImage(plan.image, exec);
|
|
11535
|
-
ensured.add(plan.image);
|
|
11536
|
-
}
|
|
11537
|
-
startContainer(plan, tablationApiBaseUrl, sync, exec);
|
|
12070
|
+
}) {
|
|
12071
|
+
const ownRoute = run3.route === routeSlug(route2.route);
|
|
12072
|
+
const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
|
|
12073
|
+
const num = run3.ticket?.replace(/^\D+/, "") ?? "";
|
|
12074
|
+
return {
|
|
12075
|
+
role: run3.role,
|
|
12076
|
+
seat: ownRoute ? seatIds[run3.role] ?? null : null,
|
|
12077
|
+
ticket: run3.ticket,
|
|
12078
|
+
ticketRecordId: run3.ticketRecordId,
|
|
12079
|
+
repo: run3.repo ?? null,
|
|
12080
|
+
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
12081
|
+
stream: run3.streamPath,
|
|
12082
|
+
pid: run3.pid,
|
|
12083
|
+
startedAt: run3.spawnedAt,
|
|
12084
|
+
deadline: run3.deadline ?? null
|
|
11538
12085
|
};
|
|
11539
|
-
for (const plan of plans) {
|
|
11540
|
-
const sync = syncEndpoints.get(plan.workspaceId);
|
|
11541
|
-
const current = existingByName.get(plan.containerName);
|
|
11542
|
-
if (!current) {
|
|
11543
|
-
start(plan, sync);
|
|
11544
|
-
started.push(plan.containerName);
|
|
11545
|
-
} else if (!current.running) {
|
|
11546
|
-
stopContainer(plan.containerName, exec);
|
|
11547
|
-
start(plan, sync);
|
|
11548
|
-
started.push(plan.containerName);
|
|
11549
|
-
} else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
|
|
11550
|
-
stopContainer(plan.containerName, exec);
|
|
11551
|
-
start(plan, sync);
|
|
11552
|
-
recreated.push(plan.containerName);
|
|
11553
|
-
}
|
|
11554
|
-
}
|
|
11555
|
-
for (const { name } of existing) {
|
|
11556
|
-
if (!plannedByName.has(name)) {
|
|
11557
|
-
stopContainer(name, exec);
|
|
11558
|
-
stopped.push(name);
|
|
11559
|
-
}
|
|
11560
|
-
}
|
|
11561
|
-
return { started, recreated, stopped };
|
|
11562
|
-
}
|
|
11563
|
-
function passengerCheckoutSyncSpecs(cfg, workspaceId) {
|
|
11564
|
-
const specs = [];
|
|
11565
|
-
for (const route2 of cfg.routes) {
|
|
11566
|
-
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
11567
|
-
if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
|
|
11568
|
-
const routeWorkspaceId = route2.resolved.workspaceId;
|
|
11569
|
-
const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
|
|
11570
|
-
for (const target of passengerCheckoutTargets(route2)) {
|
|
11571
|
-
const ordinary = ordinaryByName.get(target.name);
|
|
11572
|
-
const override = route2.repoOverrides[target.name];
|
|
11573
|
-
const effective = resolveRepoConfig(
|
|
11574
|
-
ordinary ? loadRepoConfig(ordinary.dir) : null,
|
|
11575
|
-
{ branch: { ...route2.branch, ...override?.branch } },
|
|
11576
|
-
ordinary?.dir ?? target.dir
|
|
11577
|
-
);
|
|
11578
|
-
specs.push({
|
|
11579
|
-
name: target.name,
|
|
11580
|
-
workspaceId: routeWorkspaceId,
|
|
11581
|
-
dir: target.dir,
|
|
11582
|
-
remote: target.remote,
|
|
11583
|
-
gitRemoteName: effective.branch.remote,
|
|
11584
|
-
base: effective.branch.base,
|
|
11585
|
-
originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
|
|
11586
|
-
cloneScheme: cfg.ship.cloneScheme,
|
|
11587
|
-
...target.forgeHost ? { forgeHost: target.forgeHost } : {}
|
|
11588
|
-
});
|
|
11589
|
-
}
|
|
11590
|
-
}
|
|
11591
|
-
return specs;
|
|
11592
|
-
}
|
|
11593
|
-
function syncAllPassengerCheckouts(cfg) {
|
|
11594
|
-
return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
|
|
11595
|
-
name: spec.name,
|
|
11596
|
-
workspaceId: spec.workspaceId,
|
|
11597
|
-
outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
|
|
11598
|
-
}));
|
|
11599
12086
|
}
|
|
11600
12087
|
|
|
11601
12088
|
// src/deprovision-host.ts
|
|
@@ -11629,22 +12116,22 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
|
11629
12116
|
};
|
|
11630
12117
|
return {
|
|
11631
12118
|
liveRuns: () => liveRuns(stateDir).map((r) => ({ role: r.role, route: r.route, pid: r.pid, ticketRecordId: r.ticketRecordId ?? void 0, streamPath: r.streamPath })),
|
|
11632
|
-
killRun: (
|
|
11633
|
-
recordKill(
|
|
11634
|
-
emit.emit(`run_killed: ${
|
|
12119
|
+
killRun: (run3) => killRunGroup(run3.pid),
|
|
12120
|
+
recordKill(run3) {
|
|
12121
|
+
emit.emit(`run_killed: ${run3.role} run on ${run3.route} stopped by crew deprovision`, {
|
|
11635
12122
|
step: "agent",
|
|
11636
|
-
role:
|
|
11637
|
-
data: { event: "run_killed", reason: "deprovision", pid:
|
|
12123
|
+
role: run3.role,
|
|
12124
|
+
data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
|
|
11638
12125
|
});
|
|
11639
12126
|
},
|
|
11640
|
-
async handBack(
|
|
11641
|
-
const route2 = routeFor(
|
|
11642
|
-
if (!
|
|
11643
|
-
const memberId = route2.resolved?.seats[
|
|
12127
|
+
async handBack(run3) {
|
|
12128
|
+
const route2 = routeFor(run3.route);
|
|
12129
|
+
if (!run3.ticketRecordId || !route2) return;
|
|
12130
|
+
const memberId = route2.resolved?.seats[run3.role];
|
|
11644
12131
|
if (!memberId) return;
|
|
11645
|
-
const identity = await resolveSeatKey(route2,
|
|
12132
|
+
const identity = await resolveSeatKey(route2, run3.role, getStore);
|
|
11646
12133
|
const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
|
|
11647
|
-
const t = await tracker.ticket(
|
|
12134
|
+
const t = await tracker.ticket(run3.ticketRecordId);
|
|
11648
12135
|
const target = repoTargetFor(route2, t.repo_id);
|
|
11649
12136
|
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
11650
12137
|
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
@@ -11653,7 +12140,7 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
|
11653
12140
|
if (wt) dirty = status(wt.path).length > 0;
|
|
11654
12141
|
} catch {
|
|
11655
12142
|
}
|
|
11656
|
-
await tracker.postEvent(
|
|
12143
|
+
await tracker.postEvent(run3.ticketRecordId, interruptedComment({
|
|
11657
12144
|
branch: wt?.branch ?? null,
|
|
11658
12145
|
dirty,
|
|
11659
12146
|
reason: "the host was released and wiped"
|
|
@@ -11708,9 +12195,9 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
|
11708
12195
|
}
|
|
11709
12196
|
|
|
11710
12197
|
// src/pool-member.ts
|
|
11711
|
-
import { mkdirSync as
|
|
12198
|
+
import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
|
|
11712
12199
|
import { homedir as homedir8 } from "os";
|
|
11713
|
-
import { dirname as dirname11, join as
|
|
12200
|
+
import { dirname as dirname11, join as join30 } from "path";
|
|
11714
12201
|
var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
|
|
11715
12202
|
function backoffMs(attempt) {
|
|
11716
12203
|
return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
|
|
@@ -11899,24 +12386,24 @@ async function runPoolMember(deps) {
|
|
|
11899
12386
|
}
|
|
11900
12387
|
}
|
|
11901
12388
|
function defaultMemberDir(env = process.env) {
|
|
11902
|
-
return env.CREW_POOL_DIR ??
|
|
12389
|
+
return env.CREW_POOL_DIR ?? join30(env.XDG_STATE_HOME ?? join30(homedir8(), ".local", "state"), "crew-pool-member");
|
|
11903
12390
|
}
|
|
11904
12391
|
function fileMemberStore(dir) {
|
|
11905
|
-
const file =
|
|
12392
|
+
const file = join30(dir, "member.json");
|
|
11906
12393
|
return {
|
|
11907
12394
|
read() {
|
|
11908
12395
|
try {
|
|
11909
|
-
return JSON.parse(
|
|
12396
|
+
return JSON.parse(readFileSync25(file, "utf8"));
|
|
11910
12397
|
} catch {
|
|
11911
12398
|
return {};
|
|
11912
12399
|
}
|
|
11913
12400
|
},
|
|
11914
12401
|
write(f) {
|
|
11915
|
-
|
|
12402
|
+
mkdirSync17(dirname11(file), { recursive: true, mode: 448 });
|
|
11916
12403
|
const tmp = `${file}.tmp`;
|
|
11917
|
-
|
|
12404
|
+
writeFileSync19(tmp, `${JSON.stringify(f)}
|
|
11918
12405
|
`, { mode: 384 });
|
|
11919
|
-
|
|
12406
|
+
renameSync4(tmp, file);
|
|
11920
12407
|
}
|
|
11921
12408
|
};
|
|
11922
12409
|
}
|
|
@@ -11970,18 +12457,18 @@ import { existsSync as existsSync30 } from "fs";
|
|
|
11970
12457
|
import { hostname as hostname3 } from "os";
|
|
11971
12458
|
|
|
11972
12459
|
// src/machine-id.ts
|
|
11973
|
-
import { execFileSync as
|
|
11974
|
-
import { createHash as
|
|
11975
|
-
import { existsSync as existsSync27, mkdirSync as
|
|
11976
|
-
import { dirname as dirname12, join as
|
|
12460
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
12461
|
+
import { createHash as createHash7 } from "crypto";
|
|
12462
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
|
|
12463
|
+
import { dirname as dirname12, join as join31 } from "path";
|
|
11977
12464
|
function machineIdPathFor(stateDir) {
|
|
11978
|
-
return
|
|
12465
|
+
return join31(stateDir, "machine-id");
|
|
11979
12466
|
}
|
|
11980
|
-
var sha256 = (s) =>
|
|
12467
|
+
var sha256 = (s) => createHash7("sha256").update(s).digest("hex");
|
|
11981
12468
|
function readRawMachineId(platform2 = process.platform) {
|
|
11982
12469
|
try {
|
|
11983
12470
|
if (platform2 === "darwin") {
|
|
11984
|
-
const out =
|
|
12471
|
+
const out = execFileSync7("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
11985
12472
|
encoding: "utf8",
|
|
11986
12473
|
stdio: ["ignore", "pipe", "ignore"]
|
|
11987
12474
|
});
|
|
@@ -11989,7 +12476,7 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
11989
12476
|
}
|
|
11990
12477
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
11991
12478
|
if (existsSync27(p)) {
|
|
11992
|
-
const id =
|
|
12479
|
+
const id = readFileSync26(p, "utf8").trim();
|
|
11993
12480
|
if (id) return id;
|
|
11994
12481
|
}
|
|
11995
12482
|
}
|
|
@@ -12001,14 +12488,14 @@ function ensureMachineId(stateDir, opts = {}) {
|
|
|
12001
12488
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
12002
12489
|
const path = machineIdPathFor(stateDir);
|
|
12003
12490
|
if (existsSync27(path)) {
|
|
12004
|
-
const cached =
|
|
12491
|
+
const cached = readFileSync26(path, "utf8").trim();
|
|
12005
12492
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
12006
12493
|
}
|
|
12007
12494
|
const raw = (opts.read ?? readRawMachineId)();
|
|
12008
12495
|
if (!raw) return void 0;
|
|
12009
12496
|
const hashed = sha256(raw);
|
|
12010
|
-
|
|
12011
|
-
|
|
12497
|
+
mkdirSync18(dirname12(path), { recursive: true });
|
|
12498
|
+
writeFileSync20(path, `${hashed}
|
|
12012
12499
|
`, { mode: 384 });
|
|
12013
12500
|
return hashed;
|
|
12014
12501
|
}
|
|
@@ -12021,7 +12508,7 @@ function matchShipRows(ships, shipName2, machineId) {
|
|
|
12021
12508
|
|
|
12022
12509
|
// src/provision.ts
|
|
12023
12510
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
12024
|
-
import { existsSync as existsSync28, readFileSync as
|
|
12511
|
+
import { existsSync as existsSync28, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
|
|
12025
12512
|
import { hostname as hostname2 } from "os";
|
|
12026
12513
|
import { parseDocument as parseDocument3 } from "yaml";
|
|
12027
12514
|
|
|
@@ -12030,7 +12517,7 @@ import { parseDocument as parseDocument2 } from "yaml";
|
|
|
12030
12517
|
|
|
12031
12518
|
// src/repos-cmd.ts
|
|
12032
12519
|
import { homedir as homedir9 } from "os";
|
|
12033
|
-
import { basename as
|
|
12520
|
+
import { basename as basename9, join as join32, resolve as resolve3 } from "path";
|
|
12034
12521
|
import { isMap, isSeq, parseDocument, stringify } from "yaml";
|
|
12035
12522
|
var ReposError = class extends Error {
|
|
12036
12523
|
};
|
|
@@ -12102,7 +12589,7 @@ function configuredRepos(text2, route2, base) {
|
|
|
12102
12589
|
});
|
|
12103
12590
|
}
|
|
12104
12591
|
const dir = m.get("dir");
|
|
12105
|
-
return dir ? [{ name:
|
|
12592
|
+
return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12106
12593
|
}
|
|
12107
12594
|
function cloneBaseOf(text2, route2, base, isCheckout) {
|
|
12108
12595
|
const m = routeMapOf(parseDocument(text2), route2);
|
|
@@ -12191,7 +12678,7 @@ function planRepoAdd(i) {
|
|
|
12191
12678
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
12192
12679
|
}
|
|
12193
12680
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
12194
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
12681
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
|
|
12195
12682
|
let warning;
|
|
12196
12683
|
if (!matched) {
|
|
12197
12684
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -12308,7 +12795,7 @@ function planRepoClone(i) {
|
|
|
12308
12795
|
}
|
|
12309
12796
|
const ship = doc.get("ship", true);
|
|
12310
12797
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
12311
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
12798
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join32(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
12312
12799
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
12313
12800
|
const forgeHost = i.resolved?.repoForgeHosts?.[id];
|
|
12314
12801
|
return { ...add, remote, target, ...forgeHost ? { forgeHost } : {} };
|
|
@@ -12581,13 +13068,13 @@ function realProvisionDeps(crewHome) {
|
|
|
12581
13068
|
enableRoute(route2) {
|
|
12582
13069
|
const file = configFile();
|
|
12583
13070
|
if (!file) return false;
|
|
12584
|
-
const text2 =
|
|
13071
|
+
const text2 = readFileSync27(file, "utf8");
|
|
12585
13072
|
const doc = parseDocument3(text2);
|
|
12586
13073
|
const routes = doc.get("routes");
|
|
12587
13074
|
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
12588
13075
|
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
12589
13076
|
if (entry.get("enabled") === true) return false;
|
|
12590
|
-
|
|
13077
|
+
writeFileSync21(file, planEnable(text2, route2));
|
|
12591
13078
|
return true;
|
|
12592
13079
|
},
|
|
12593
13080
|
installState() {
|
|
@@ -12607,7 +13094,7 @@ function realProvisionDeps(crewHome) {
|
|
|
12607
13094
|
continue;
|
|
12608
13095
|
}
|
|
12609
13096
|
for (const p of plan.unitPaths) {
|
|
12610
|
-
if (!existsSync28(p) ||
|
|
13097
|
+
if (!existsSync28(p) || readFileSync27(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
|
|
12611
13098
|
}
|
|
12612
13099
|
}
|
|
12613
13100
|
const label = labelFor(crewHome, "run");
|
|
@@ -12631,10 +13118,10 @@ function realProvisionDeps(crewHome) {
|
|
|
12631
13118
|
setShipSetting(key, value2) {
|
|
12632
13119
|
const file = configFile();
|
|
12633
13120
|
if (!file) return false;
|
|
12634
|
-
const doc = parseDocument3(
|
|
13121
|
+
const doc = parseDocument3(readFileSync27(file, "utf8"));
|
|
12635
13122
|
if (doc.getIn(["ship", key]) === value2) return false;
|
|
12636
13123
|
doc.setIn(["ship", key], value2);
|
|
12637
|
-
|
|
13124
|
+
writeFileSync21(file, doc.toString());
|
|
12638
13125
|
return true;
|
|
12639
13126
|
},
|
|
12640
13127
|
storedToken(host) {
|
|
@@ -12650,12 +13137,12 @@ host=${host}
|
|
|
12650
13137
|
}
|
|
12651
13138
|
|
|
12652
13139
|
// src/tunnel.ts
|
|
12653
|
-
import { spawn as
|
|
12654
|
-
import { existsSync as existsSync29, mkdirSync as
|
|
13140
|
+
import { spawn as spawn6 } from "child_process";
|
|
13141
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
|
|
12655
13142
|
import { randomInt } from "crypto";
|
|
12656
|
-
import { join as
|
|
13143
|
+
import { join as join33 } from "path";
|
|
12657
13144
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
12658
|
-
var defaultSpawn = (cmd, args) =>
|
|
13145
|
+
var defaultSpawn = (cmd, args) => spawn6(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
12659
13146
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
12660
13147
|
var SLUG_LENGTH = 12;
|
|
12661
13148
|
function generateTunnelSlug() {
|
|
@@ -12667,12 +13154,12 @@ function bindAddrFor(o, path) {
|
|
|
12667
13154
|
const base = `workspace-${o.workspaceId}-slug-${o.slug}${path ? `-path-${path}` : ""}-key-${o.apiKey}`;
|
|
12668
13155
|
return o.mcpSecret && !path ? `${base}-secret-${o.mcpSecret}` : base;
|
|
12669
13156
|
}
|
|
12670
|
-
function relayBaseUrlFor(
|
|
13157
|
+
function relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
|
|
12671
13158
|
const domain = relayPublicDomain ?? relayHost.replace(/^.*@/, "");
|
|
12672
|
-
return `https://${
|
|
13159
|
+
return `https://${slug2}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
|
|
12673
13160
|
}
|
|
12674
|
-
function publicUrlFor(
|
|
12675
|
-
return `${relayBaseUrlFor(
|
|
13161
|
+
function publicUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
|
|
13162
|
+
return `${relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
|
|
12676
13163
|
}
|
|
12677
13164
|
function sshArgsFor(o) {
|
|
12678
13165
|
const forwards = [];
|
|
@@ -12725,21 +13212,21 @@ function startTunnel(opts) {
|
|
|
12725
13212
|
};
|
|
12726
13213
|
}
|
|
12727
13214
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
12728
|
-
return
|
|
13215
|
+
return join33(stateDir, "passengers", `${workspaceId}.json`);
|
|
12729
13216
|
}
|
|
12730
13217
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12731
13218
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12732
13219
|
if (!existsSync29(path)) return void 0;
|
|
12733
13220
|
try {
|
|
12734
|
-
return JSON.parse(
|
|
13221
|
+
return JSON.parse(readFileSync28(path, "utf8"));
|
|
12735
13222
|
} catch {
|
|
12736
13223
|
return void 0;
|
|
12737
13224
|
}
|
|
12738
13225
|
}
|
|
12739
13226
|
function writePersistedTunnel(stateDir, state) {
|
|
12740
13227
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
12741
|
-
|
|
12742
|
-
|
|
13228
|
+
mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
|
|
13229
|
+
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
12743
13230
|
`);
|
|
12744
13231
|
}
|
|
12745
13232
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -12773,8 +13260,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12773
13260
|
const controlUnchanged = existing?.controlSocket === deps.controlSocket;
|
|
12774
13261
|
const currentSecret = mcpSecrets.get(plan.workspaceId);
|
|
12775
13262
|
const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
|
|
12776
|
-
const
|
|
12777
|
-
const slugUnchanged = existing?.liveSlug ===
|
|
13263
|
+
const slug2 = existing?.slug ?? generateTunnelSlug();
|
|
13264
|
+
const slugUnchanged = existing?.liveSlug === slug2;
|
|
12778
13265
|
if (existing && alive(existing.pid) && existing.mountsHash === mountsHash && secretUnchanged && slugUnchanged && controlUnchanged) {
|
|
12779
13266
|
if (existing.status === "connecting") {
|
|
12780
13267
|
const settled = { ...existing, status: "connected" };
|
|
@@ -12799,7 +13286,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12799
13286
|
relayHost,
|
|
12800
13287
|
relayPort,
|
|
12801
13288
|
workspaceId: plan.workspaceId,
|
|
12802
|
-
slug,
|
|
13289
|
+
slug: slug2,
|
|
12803
13290
|
apiKey,
|
|
12804
13291
|
localPort: plan.port,
|
|
12805
13292
|
controlSocket: deps.controlSocket,
|
|
@@ -12819,8 +13306,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12819
13306
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12820
13307
|
status: settledStatus,
|
|
12821
13308
|
mcpSecret: currentSecret,
|
|
12822
|
-
slug,
|
|
12823
|
-
liveSlug:
|
|
13309
|
+
slug: slug2,
|
|
13310
|
+
liveSlug: slug2,
|
|
12824
13311
|
endpointId: existing?.endpointId,
|
|
12825
13312
|
registeredKey: existing?.registeredKey
|
|
12826
13313
|
};
|
|
@@ -12841,7 +13328,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12841
13328
|
}
|
|
12842
13329
|
}
|
|
12843
13330
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
12844
|
-
const dir =
|
|
13331
|
+
const dir = join33(stateDir, "passengers");
|
|
12845
13332
|
if (!existsSync29(dir)) return [];
|
|
12846
13333
|
return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
12847
13334
|
}
|
|
@@ -12924,8 +13411,8 @@ function realPoolMemberDeps(o) {
|
|
|
12924
13411
|
if (!cfg?.ship.relayHost) return null;
|
|
12925
13412
|
for (const r of cfg.routes) {
|
|
12926
13413
|
const ws = r.resolved?.workspaceId;
|
|
12927
|
-
const
|
|
12928
|
-
if (
|
|
13414
|
+
const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
13415
|
+
if (slug2) return relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
12929
13416
|
}
|
|
12930
13417
|
return null;
|
|
12931
13418
|
},
|
|
@@ -13708,8 +14195,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
13708
14195
|
}
|
|
13709
14196
|
|
|
13710
14197
|
// src/poll.ts
|
|
13711
|
-
import { writeFileSync as
|
|
13712
|
-
import { join as
|
|
14198
|
+
import { writeFileSync as writeFileSync23 } from "fs";
|
|
14199
|
+
import { join as join34 } from "path";
|
|
13713
14200
|
|
|
13714
14201
|
// src/stalled.ts
|
|
13715
14202
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -14016,7 +14503,7 @@ async function decideCycle(o) {
|
|
|
14016
14503
|
}
|
|
14017
14504
|
return decision;
|
|
14018
14505
|
}
|
|
14019
|
-
var digestPath = (stateDir, route2, role, runId) =>
|
|
14506
|
+
var digestPath = (stateDir, route2, role, runId) => join34(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
14020
14507
|
function otherRunsRemoved(tickets, own) {
|
|
14021
14508
|
if (!own) return tickets;
|
|
14022
14509
|
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
@@ -14094,7 +14581,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
14094
14581
|
|
|
14095
14582
|
${body}` : body;
|
|
14096
14583
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
14097
|
-
|
|
14584
|
+
writeFileSync23(path, text2);
|
|
14098
14585
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
|
|
14099
14586
|
data: { role, bytes: Buffer.byteLength(text2) }
|
|
14100
14587
|
});
|
|
@@ -14202,7 +14689,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
|
|
|
14202
14689
|
}
|
|
14203
14690
|
|
|
14204
14691
|
// src/repo-refresh.ts
|
|
14205
|
-
import { existsSync as existsSync31, mkdirSync as
|
|
14692
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
|
|
14206
14693
|
import { dirname as dirname14 } from "path";
|
|
14207
14694
|
var REFRESH_COOLDOWN_MS = 2 * 6e4;
|
|
14208
14695
|
var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
|
|
@@ -14233,10 +14720,10 @@ function applyRepoRows(resolved, rows) {
|
|
|
14233
14720
|
function persistRepoRows(stateDir, routeName, rows) {
|
|
14234
14721
|
const path = resolvedPathFor(stateDir, routeName);
|
|
14235
14722
|
if (!existsSync31(path)) return;
|
|
14236
|
-
const raw = JSON.parse(
|
|
14723
|
+
const raw = JSON.parse(readFileSync29(path, "utf8"));
|
|
14237
14724
|
applyRepoRows(raw, rows);
|
|
14238
|
-
|
|
14239
|
-
|
|
14725
|
+
mkdirSync20(dirname14(path), { recursive: true });
|
|
14726
|
+
writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
|
|
14240
14727
|
`);
|
|
14241
14728
|
}
|
|
14242
14729
|
async function refreshUnknownRepos(o) {
|
|
@@ -14686,8 +15173,8 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
14686
15173
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
14687
15174
|
|
|
14688
15175
|
// src/connect-wizard.ts
|
|
14689
|
-
import { delimiter, dirname as dirname15, join as
|
|
14690
|
-
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as
|
|
15176
|
+
import { delimiter, dirname as dirname15, join as join35 } from "path";
|
|
15177
|
+
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync25 } from "fs";
|
|
14691
15178
|
import { stringify as stringify2 } from "yaml";
|
|
14692
15179
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
14693
15180
|
function shouldRunWizard(opts) {
|
|
@@ -14709,7 +15196,7 @@ function defaultIsExecutable(p) {
|
|
|
14709
15196
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
14710
15197
|
for (const dir of pathEnv.split(delimiter)) {
|
|
14711
15198
|
if (!dir) continue;
|
|
14712
|
-
const candidate =
|
|
15199
|
+
const candidate = join35(dir, name);
|
|
14713
15200
|
if (isExecutable(candidate)) return candidate;
|
|
14714
15201
|
}
|
|
14715
15202
|
return void 0;
|
|
@@ -14770,9 +15257,9 @@ routes:
|
|
|
14770
15257
|
${routeBlock}`;
|
|
14771
15258
|
}
|
|
14772
15259
|
function writeNewConfig(path, text2) {
|
|
14773
|
-
|
|
15260
|
+
mkdirSync21(dirname15(path), { recursive: true });
|
|
14774
15261
|
try {
|
|
14775
|
-
|
|
15262
|
+
writeFileSync25(path, text2, { mode: 384, flag: "wx" });
|
|
14776
15263
|
return true;
|
|
14777
15264
|
} catch (e) {
|
|
14778
15265
|
if (e.code === "EEXIST") return false;
|
|
@@ -14806,7 +15293,7 @@ Wrote ${o.configPath}.
|
|
|
14806
15293
|
// src/connect-json.ts
|
|
14807
15294
|
import { accessSync as accessSync4, constants as constants4 } from "fs";
|
|
14808
15295
|
import { homedir as homedir10 } from "os";
|
|
14809
|
-
import { dirname as dirname16, join as
|
|
15296
|
+
import { dirname as dirname16, join as join36 } from "path";
|
|
14810
15297
|
import { delimiter as delimiter2 } from "path";
|
|
14811
15298
|
var CHOICE_EXIT_CODE = 3;
|
|
14812
15299
|
function createEmitter(json2, write) {
|
|
@@ -14845,12 +15332,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
|
14845
15332
|
const dirs = [
|
|
14846
15333
|
...pathEnv.split(delimiter2),
|
|
14847
15334
|
...(extraPath ?? "").split(delimiter2),
|
|
14848
|
-
|
|
14849
|
-
|
|
15335
|
+
join36(home, ".local/bin"),
|
|
15336
|
+
join36(home, ".claude/local"),
|
|
14850
15337
|
"/opt/homebrew/bin",
|
|
14851
15338
|
"/usr/local/bin",
|
|
14852
|
-
|
|
14853
|
-
|
|
15339
|
+
join36(home, ".npm-global/bin"),
|
|
15340
|
+
join36(home, ".volta/bin")
|
|
14854
15341
|
];
|
|
14855
15342
|
return [...new Set(dirs.filter(Boolean))];
|
|
14856
15343
|
}
|
|
@@ -14886,9 +15373,9 @@ function flagWizardAnswers(f, env) {
|
|
|
14886
15373
|
}
|
|
14887
15374
|
|
|
14888
15375
|
// src/release-run.ts
|
|
14889
|
-
import { readFileSync as
|
|
14890
|
-
import { dirname as dirname17, join as
|
|
14891
|
-
import { tmpdir as
|
|
15376
|
+
import { readFileSync as readFileSync30, writeFileSync as writeFileSync26, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, rmSync as rmSync17 } from "fs";
|
|
15377
|
+
import { dirname as dirname17, join as join37 } from "path";
|
|
15378
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
14892
15379
|
|
|
14893
15380
|
// src/release.ts
|
|
14894
15381
|
function describeUnplaceable(unplaceable, verifiedStatus) {
|
|
@@ -15628,13 +16115,13 @@ async function bump(o, size) {
|
|
|
15628
16115
|
}
|
|
15629
16116
|
return printed;
|
|
15630
16117
|
}
|
|
15631
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
16118
|
+
const files = o.repo.release.versionFiles.map((f) => join37(o.cwd, f));
|
|
15632
16119
|
const first = files[0];
|
|
15633
16120
|
if (!existsSync33(first)) {
|
|
15634
16121
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
15635
16122
|
return null;
|
|
15636
16123
|
}
|
|
15637
|
-
const current = JSON.parse(
|
|
16124
|
+
const current = JSON.parse(readFileSync30(first, "utf8")).version;
|
|
15638
16125
|
if (!current) {
|
|
15639
16126
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
15640
16127
|
return null;
|
|
@@ -15647,27 +16134,27 @@ async function bump(o, size) {
|
|
|
15647
16134
|
return version;
|
|
15648
16135
|
}
|
|
15649
16136
|
for (const f of files) {
|
|
15650
|
-
const src =
|
|
16137
|
+
const src = readFileSync30(f, "utf8");
|
|
15651
16138
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
15652
16139
|
if (out === src) {
|
|
15653
16140
|
o.emit.error(`no version field in ${f}`);
|
|
15654
16141
|
return null;
|
|
15655
16142
|
}
|
|
15656
|
-
|
|
16143
|
+
writeFileSync26(f, out);
|
|
15657
16144
|
}
|
|
15658
16145
|
return version;
|
|
15659
16146
|
}
|
|
15660
16147
|
function writeChangelog(o, version, entries) {
|
|
15661
16148
|
const file = o.repo.release.changelog;
|
|
15662
16149
|
if (!file || entries.length === 0) return;
|
|
15663
|
-
const path =
|
|
16150
|
+
const path = join37(o.cwd, file);
|
|
15664
16151
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
15665
16152
|
if (o.dryRun) {
|
|
15666
16153
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
15667
16154
|
return;
|
|
15668
16155
|
}
|
|
15669
|
-
const existing = existsSync33(path) ?
|
|
15670
|
-
|
|
16156
|
+
const existing = existsSync33(path) ? readFileSync30(path, "utf8") : "# Changelog\n";
|
|
16157
|
+
writeFileSync26(path, insertChangelogSection(existing, section));
|
|
15671
16158
|
}
|
|
15672
16159
|
async function confirm(o, expected) {
|
|
15673
16160
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -15745,23 +16232,23 @@ async function confirmLastRelease(o, decision) {
|
|
|
15745
16232
|
if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
|
|
15746
16233
|
return { confirmed, version };
|
|
15747
16234
|
}
|
|
15748
|
-
function closeReleaseCheckout(cwd, root, dir, branch,
|
|
16235
|
+
function closeReleaseCheckout(cwd, root, dir, branch, run3 = gitOk) {
|
|
15749
16236
|
try {
|
|
15750
|
-
|
|
16237
|
+
run3(cwd, ["worktree", "remove", "--force", dir]);
|
|
15751
16238
|
} finally {
|
|
15752
16239
|
try {
|
|
15753
|
-
|
|
16240
|
+
rmSync17(root, { recursive: true, force: true });
|
|
15754
16241
|
} finally {
|
|
15755
|
-
|
|
15756
|
-
|
|
16242
|
+
run3(cwd, ["worktree", "prune"]);
|
|
16243
|
+
run3(cwd, ["branch", "-D", branch]);
|
|
15757
16244
|
}
|
|
15758
16245
|
}
|
|
15759
16246
|
}
|
|
15760
16247
|
function openReleaseCheckout(o) {
|
|
15761
16248
|
const { remote, base } = o.repo.branch;
|
|
15762
16249
|
const branch = `crew/release-${Date.now()}`;
|
|
15763
|
-
const root =
|
|
15764
|
-
const dir =
|
|
16250
|
+
const root = mkdtempSync3(join37(tmpdir5(), "crew-release-"));
|
|
16251
|
+
const dir = join37(root, "checkout");
|
|
15765
16252
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
15766
16253
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
15767
16254
|
try {
|
|
@@ -15771,10 +16258,10 @@ function openReleaseCheckout(o) {
|
|
|
15771
16258
|
throw e;
|
|
15772
16259
|
}
|
|
15773
16260
|
for (const f of o.repo.worktrees.copy) {
|
|
15774
|
-
const src =
|
|
16261
|
+
const src = join37(o.cwd, f);
|
|
15775
16262
|
if (!existsSync33(src)) continue;
|
|
15776
|
-
|
|
15777
|
-
copyFileSync(src,
|
|
16263
|
+
mkdirSync22(dirname17(join37(dir, f)), { recursive: true });
|
|
16264
|
+
copyFileSync(src, join37(dir, f));
|
|
15778
16265
|
}
|
|
15779
16266
|
return { dir, branch, close };
|
|
15780
16267
|
}
|
|
@@ -15919,7 +16406,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
15919
16406
|
let checkout;
|
|
15920
16407
|
const stranded = recoverStrandedReleaseCheckouts(o.cwd);
|
|
15921
16408
|
if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
|
|
15922
|
-
const swept = sweepStaleReleaseRoots(
|
|
16409
|
+
const swept = sweepStaleReleaseRoots(tmpdir5());
|
|
15923
16410
|
if (swept.length > 0) o.emit.emit(scoped(`removed ${swept.length} stale crew-release-* temp dir(s)`), { step: "release" });
|
|
15924
16411
|
try {
|
|
15925
16412
|
checkout = openReleaseCheckout(o);
|
|
@@ -17092,7 +17579,7 @@ function nextRoles(decision, ran) {
|
|
|
17092
17579
|
}
|
|
17093
17580
|
|
|
17094
17581
|
// src/ship-name.ts
|
|
17095
|
-
import { existsSync as existsSync35, mkdirSync as
|
|
17582
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
|
|
17096
17583
|
import { dirname as dirname18 } from "path";
|
|
17097
17584
|
import { parseDocument as parseDocument5 } from "yaml";
|
|
17098
17585
|
var ShipNameError = class extends Error {
|
|
@@ -17156,7 +17643,7 @@ async function syncRouteName(i) {
|
|
|
17156
17643
|
function readSyncedName(resolvedFile) {
|
|
17157
17644
|
if (!existsSync35(resolvedFile)) return void 0;
|
|
17158
17645
|
try {
|
|
17159
|
-
const v = JSON.parse(
|
|
17646
|
+
const v = JSON.parse(readFileSync31(resolvedFile, "utf8")).shipName;
|
|
17160
17647
|
return typeof v === "string" ? v : void 0;
|
|
17161
17648
|
} catch {
|
|
17162
17649
|
return void 0;
|
|
@@ -17166,22 +17653,22 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
17166
17653
|
if (!existsSync35(resolvedFile)) return;
|
|
17167
17654
|
let raw;
|
|
17168
17655
|
try {
|
|
17169
|
-
raw = JSON.parse(
|
|
17656
|
+
raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
17170
17657
|
} catch {
|
|
17171
17658
|
return;
|
|
17172
17659
|
}
|
|
17173
17660
|
if (raw.shipName === name) return;
|
|
17174
17661
|
raw.shipName = name;
|
|
17175
|
-
|
|
17662
|
+
mkdirSync23(dirname18(resolvedFile), { recursive: true });
|
|
17176
17663
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
17177
|
-
|
|
17664
|
+
writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
|
|
17178
17665
|
`);
|
|
17179
|
-
|
|
17666
|
+
renameSync5(tmp, resolvedFile);
|
|
17180
17667
|
}
|
|
17181
17668
|
async function reconcileShipName(i) {
|
|
17182
17669
|
let local = i.ship.name;
|
|
17183
17670
|
try {
|
|
17184
|
-
local = shipNameOfYaml(
|
|
17671
|
+
local = shipNameOfYaml(readFileSync31(i.configFile, "utf8")) ?? local;
|
|
17185
17672
|
} catch {
|
|
17186
17673
|
}
|
|
17187
17674
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -17205,8 +17692,8 @@ async function reconcileShipName(i) {
|
|
|
17205
17692
|
}
|
|
17206
17693
|
if (result.status !== "ok") return result;
|
|
17207
17694
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
17208
|
-
const text2 =
|
|
17209
|
-
|
|
17695
|
+
const text2 = readFileSync31(i.configFile, "utf8");
|
|
17696
|
+
writeFileSync27(i.configFile, setShipNameInYaml(text2, result.name));
|
|
17210
17697
|
i.ship.name = result.name;
|
|
17211
17698
|
if (result.action === "row-wins") {
|
|
17212
17699
|
i.emit?.warn(
|
|
@@ -17865,7 +18352,7 @@ function describeCapability(i) {
|
|
|
17865
18352
|
}
|
|
17866
18353
|
|
|
17867
18354
|
// src/agents-sync-run.ts
|
|
17868
|
-
import { existsSync as existsSync36, mkdirSync as
|
|
18355
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
|
|
17869
18356
|
import { dirname as dirname19 } from "path";
|
|
17870
18357
|
async function runAgentsSync(target, o) {
|
|
17871
18358
|
const dryRun = o.dryRun ?? false;
|
|
@@ -17899,12 +18386,12 @@ async function runAgentsSync(target, o) {
|
|
|
17899
18386
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
17900
18387
|
if (!dryRun) {
|
|
17901
18388
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
17902
|
-
const raw = existsSync36(resolvedPath) ? JSON.parse(
|
|
18389
|
+
const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync32(resolvedPath, "utf8")) : {};
|
|
17903
18390
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
17904
18391
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
17905
18392
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
17906
|
-
|
|
17907
|
-
|
|
18393
|
+
mkdirSync24(dirname19(resolvedPath), { recursive: true });
|
|
18394
|
+
writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
17908
18395
|
`);
|
|
17909
18396
|
if (target.resolved) {
|
|
17910
18397
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -17919,7 +18406,7 @@ async function runAgentsSync(target, o) {
|
|
|
17919
18406
|
}
|
|
17920
18407
|
|
|
17921
18408
|
// src/logbook.ts
|
|
17922
|
-
import { createHash as
|
|
18409
|
+
import { createHash as createHash8 } from "crypto";
|
|
17923
18410
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
17924
18411
|
var LogbookError = class extends Error {
|
|
17925
18412
|
};
|
|
@@ -17992,7 +18479,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
17992
18479
|
};
|
|
17993
18480
|
}
|
|
17994
18481
|
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
17995
|
-
const actualSha =
|
|
18482
|
+
const actualSha = createHash8("sha256").update(prompt).digest("hex");
|
|
17996
18483
|
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
17997
18484
|
return {
|
|
17998
18485
|
entry,
|
|
@@ -18008,31 +18495,31 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
18008
18495
|
|
|
18009
18496
|
// src/repos-check.ts
|
|
18010
18497
|
import { spawnSync as spawnSync9 } from "child_process";
|
|
18011
|
-
import { readFileSync as
|
|
18012
|
-
import { basename as
|
|
18498
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
18499
|
+
import { basename as basename10 } from "path";
|
|
18013
18500
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
18014
18501
|
function checkHookSyntax(script, shell) {
|
|
18015
18502
|
const spec = resolveShell(shell);
|
|
18016
|
-
const name =
|
|
18017
|
-
let
|
|
18503
|
+
const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
18504
|
+
let run3;
|
|
18018
18505
|
if (POSIX_SHELL.test(name)) {
|
|
18019
|
-
|
|
18506
|
+
run3 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
18020
18507
|
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
18021
18508
|
const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
|
|
18022
|
-
|
|
18509
|
+
run3 = spawnSync9(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
18023
18510
|
encoding: "utf8",
|
|
18024
18511
|
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
18025
18512
|
});
|
|
18026
18513
|
} else {
|
|
18027
18514
|
return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
|
|
18028
18515
|
}
|
|
18029
|
-
if (
|
|
18030
|
-
if (
|
|
18031
|
-
const message = `${
|
|
18516
|
+
if (run3.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run3.error.message}` };
|
|
18517
|
+
if (run3.status === 0) return { status: "ok" };
|
|
18518
|
+
const message = `${run3.stderr ?? ""}${run3.stdout ?? ""}`.trim() || `${spec.bin} exited ${run3.status}`;
|
|
18032
18519
|
return { status: "error", message };
|
|
18033
18520
|
}
|
|
18034
18521
|
function renderSamples(cfg, dir) {
|
|
18035
|
-
const checkout =
|
|
18522
|
+
const checkout = basename10(dir.replace(/[/\\]+$/, ""));
|
|
18036
18523
|
const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
|
|
18037
18524
|
const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
|
|
18038
18525
|
return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
|
|
@@ -18049,7 +18536,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
18049
18536
|
}
|
|
18050
18537
|
let parsed;
|
|
18051
18538
|
try {
|
|
18052
|
-
parsed = parseRepoConfig(
|
|
18539
|
+
parsed = parseRepoConfig(readFileSync33(file, "utf8"), file);
|
|
18053
18540
|
} catch (e) {
|
|
18054
18541
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
18055
18542
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -18324,7 +18811,7 @@ function existingRepoConfig(f) {
|
|
|
18324
18811
|
}
|
|
18325
18812
|
|
|
18326
18813
|
// src/repos-hooks-agent.ts
|
|
18327
|
-
import { spawn as
|
|
18814
|
+
import { spawn as spawn7, execFileSync as execFileSync8 } from "child_process";
|
|
18328
18815
|
import { realpathSync as realpathSync2 } from "fs";
|
|
18329
18816
|
import { isAbsolute as isAbsolute2, relative } from "path";
|
|
18330
18817
|
var AgentSuggestError = class extends Error {
|
|
@@ -18427,7 +18914,7 @@ function classifyFailure(o) {
|
|
|
18427
18914
|
}
|
|
18428
18915
|
function porcelain(dir) {
|
|
18429
18916
|
try {
|
|
18430
|
-
return
|
|
18917
|
+
return execFileSync8("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
|
|
18431
18918
|
} catch {
|
|
18432
18919
|
return void 0;
|
|
18433
18920
|
}
|
|
@@ -18442,10 +18929,10 @@ async function runAgentSuggest(o) {
|
|
|
18442
18929
|
const before = porcelain(o.dir);
|
|
18443
18930
|
const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
18444
18931
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
18445
|
-
const
|
|
18932
|
+
const run3 = await new Promise((resolve7) => {
|
|
18446
18933
|
let out = "";
|
|
18447
18934
|
let err3 = "";
|
|
18448
|
-
const child =
|
|
18935
|
+
const child = spawn7(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
18449
18936
|
let settled = false;
|
|
18450
18937
|
const settle = (r) => {
|
|
18451
18938
|
if (settled) return;
|
|
@@ -18477,18 +18964,18 @@ async function runAgentSuggest(o) {
|
|
|
18477
18964
|
});
|
|
18478
18965
|
child.stdin.end(buildAgentPrompt(o.sniff));
|
|
18479
18966
|
});
|
|
18480
|
-
if (
|
|
18967
|
+
if (run3.aborted || signal.aborted) {
|
|
18481
18968
|
throw new AgentSuggestError(o.signal?.aborted ? "cancelled" : "timeout", o.signal?.aborted ? "cancelled" : "the toolchain did not answer in time");
|
|
18482
18969
|
}
|
|
18483
18970
|
const after = porcelain(o.dir);
|
|
18484
18971
|
if (before !== void 0 && after !== before) {
|
|
18485
18972
|
throw new AgentSuggestError("modified_checkout", "the checkout changed during --agent; inspect it with `git status` (the agent result was discarded)");
|
|
18486
18973
|
}
|
|
18487
|
-
const parsed = parseAgentStream(
|
|
18488
|
-
if (
|
|
18489
|
-
const reason = classifyFailure({ code:
|
|
18490
|
-
${
|
|
18491
|
-
throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text ||
|
|
18974
|
+
const parsed = parseAgentStream(run3.out, o.dir);
|
|
18975
|
+
if (run3.spawnError || run3.code !== 0 || parsed.isError) {
|
|
18976
|
+
const reason = classifyFailure({ code: run3.code, ...run3.spawnError ? { spawnError: run3.spawnError } : {}, output: `${parsed.text}
|
|
18977
|
+
${run3.err}` });
|
|
18978
|
+
throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run3.err).trim().slice(0, 300) || `exited ${run3.code}`);
|
|
18492
18979
|
}
|
|
18493
18980
|
const { hooks, notes } = parseAgentHooks(parsed.text);
|
|
18494
18981
|
const differsFromSniff = HOOK_KEYS2.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
|
|
@@ -18496,7 +18983,7 @@ ${run2.err}` });
|
|
|
18496
18983
|
}
|
|
18497
18984
|
|
|
18498
18985
|
// src/sample-ticket.ts
|
|
18499
|
-
import { basename as
|
|
18986
|
+
import { basename as basename11, dirname as dirname20, join as join38 } from "path";
|
|
18500
18987
|
var SAMPLE_REPO = "crew-sample";
|
|
18501
18988
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
18502
18989
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -18519,8 +19006,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
18519
19006
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
18520
19007
|
`;
|
|
18521
19008
|
function sampleDir(existing, reposBase, workspace) {
|
|
18522
|
-
if (existing.length > 0) return
|
|
18523
|
-
return
|
|
19009
|
+
if (existing.length > 0) return join38(dirname20(existing[0]), SAMPLE_REPO);
|
|
19010
|
+
return join38(reposBase, workspace, SAMPLE_REPO);
|
|
18524
19011
|
}
|
|
18525
19012
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
18526
19013
|
function sampleDescription() {
|
|
@@ -18564,8 +19051,8 @@ function planSampleTicket(i) {
|
|
|
18564
19051
|
}
|
|
18565
19052
|
|
|
18566
19053
|
// src/worktree-sweep.ts
|
|
18567
|
-
import { existsSync as existsSync37, readFileSync as
|
|
18568
|
-
import { basename as
|
|
19054
|
+
import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync14 } from "fs";
|
|
19055
|
+
import { basename as basename12, dirname as dirname21, join as join39, resolve as resolve4 } from "path";
|
|
18569
19056
|
function planWorktreeSweep(target, tickets, contract) {
|
|
18570
19057
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
18571
19058
|
const verified = contract.statuses.verified;
|
|
@@ -18606,7 +19093,7 @@ function ticketForWorktree(w, byNumber, contract) {
|
|
|
18606
19093
|
const t = num ? byNumber.get(num) : void 0;
|
|
18607
19094
|
if (t && ticketStems(t).has(stem)) return t;
|
|
18608
19095
|
}
|
|
18609
|
-
const dir =
|
|
19096
|
+
const dir = basename12(w.path);
|
|
18610
19097
|
const m = DIR_NUMBER.exec(dir);
|
|
18611
19098
|
if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
|
|
18612
19099
|
if (!w.branch) {
|
|
@@ -18622,7 +19109,7 @@ function worktreeTicketNumbers(target) {
|
|
|
18622
19109
|
const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
|
|
18623
19110
|
if (n) nums.add(n);
|
|
18624
19111
|
}
|
|
18625
|
-
const m = DIR_NUMBER_SUFFIXED.exec(
|
|
19112
|
+
const m = DIR_NUMBER_SUFFIXED.exec(basename12(w.path));
|
|
18626
19113
|
if (m) nums.add(m[1]);
|
|
18627
19114
|
}
|
|
18628
19115
|
for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
|
|
@@ -18630,7 +19117,7 @@ function worktreeTicketNumbers(target) {
|
|
|
18630
19117
|
}
|
|
18631
19118
|
function siblingWorktreeDirs(target) {
|
|
18632
19119
|
const parent = dirname21(target.dir.replace(/[/\\]+$/, ""));
|
|
18633
|
-
const stem = `${
|
|
19120
|
+
const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
18634
19121
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
18635
19122
|
let names;
|
|
18636
19123
|
try {
|
|
@@ -18642,7 +19129,7 @@ function siblingWorktreeDirs(target) {
|
|
|
18642
19129
|
for (const name of names) {
|
|
18643
19130
|
if (!name.startsWith(stem)) continue;
|
|
18644
19131
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
18645
|
-
const path =
|
|
19132
|
+
const path = join39(parent, name);
|
|
18646
19133
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
18647
19134
|
}
|
|
18648
19135
|
return out;
|
|
@@ -18655,7 +19142,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
|
|
|
18655
19142
|
}
|
|
18656
19143
|
function warnOrphanedWorktreeDirs(paths, log, held) {
|
|
18657
19144
|
for (const p of paths) {
|
|
18658
|
-
const number = DIR_NUMBER_SUFFIXED.exec(
|
|
19145
|
+
const number = DIR_NUMBER_SUFFIXED.exec(basename12(p))?.[1];
|
|
18659
19146
|
const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
|
|
18660
19147
|
if (when) {
|
|
18661
19148
|
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" });
|
|
@@ -18745,7 +19232,7 @@ function isHandBackMerge(path, base) {
|
|
|
18745
19232
|
if (!msgPath) return false;
|
|
18746
19233
|
let first;
|
|
18747
19234
|
try {
|
|
18748
|
-
first =
|
|
19235
|
+
first = readFileSync34(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
18749
19236
|
} catch {
|
|
18750
19237
|
return false;
|
|
18751
19238
|
}
|
|
@@ -18799,7 +19286,7 @@ function isTicketBranch(name, base, stems) {
|
|
|
18799
19286
|
}
|
|
18800
19287
|
function sweepCandidatesExist(cwd, base) {
|
|
18801
19288
|
const linked = worktrees(cwd).slice(1);
|
|
18802
|
-
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(
|
|
19289
|
+
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename12(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
|
|
18803
19290
|
if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
|
|
18804
19291
|
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
18805
19292
|
}
|
|
@@ -18850,14 +19337,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
18850
19337
|
}
|
|
18851
19338
|
|
|
18852
19339
|
// src/stream-sweep.ts
|
|
18853
|
-
import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as
|
|
18854
|
-
import { join as
|
|
19340
|
+
import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync18, statSync as statSync10 } from "fs";
|
|
19341
|
+
import { join as join40 } from "path";
|
|
18855
19342
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
18856
19343
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
18857
|
-
const dir =
|
|
19344
|
+
const dir = join40(stateDir, "streams");
|
|
18858
19345
|
if (!existsSync38(dir)) return [];
|
|
18859
19346
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
18860
|
-
return readdirSync15(dir).map((name) =>
|
|
19347
|
+
return readdirSync15(dir).map((name) => join40(dir, name)).filter((path) => {
|
|
18861
19348
|
try {
|
|
18862
19349
|
return statSync10(path).mtimeMs < cutoff;
|
|
18863
19350
|
} catch {
|
|
@@ -18873,7 +19360,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
18873
19360
|
continue;
|
|
18874
19361
|
}
|
|
18875
19362
|
try {
|
|
18876
|
-
|
|
19363
|
+
rmSync18(path, { force: true });
|
|
18877
19364
|
removed++;
|
|
18878
19365
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
18879
19366
|
} catch (e) {
|
|
@@ -18885,7 +19372,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
18885
19372
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
18886
19373
|
if (!existsSync38(stateDir)) return [];
|
|
18887
19374
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
18888
|
-
return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
19375
|
+
return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join40(stateDir, name)).filter((path) => {
|
|
18889
19376
|
try {
|
|
18890
19377
|
return statSync10(path).mtimeMs < cutoff;
|
|
18891
19378
|
} catch {
|
|
@@ -18895,12 +19382,12 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
18895
19382
|
}
|
|
18896
19383
|
|
|
18897
19384
|
// src/cli.ts
|
|
18898
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
19385
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync40, existsSync as existsSync45, mkdirSync as mkdirSync30, writeFileSync as writeFileSync33, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync14, readdirSync as readdirSync20 } from "fs";
|
|
18899
19386
|
import { createInterface } from "readline/promises";
|
|
18900
19387
|
|
|
18901
19388
|
// src/periscope-cmd.ts
|
|
18902
|
-
import { existsSync as existsSync39, mkdirSync as
|
|
18903
|
-
import { join as
|
|
19389
|
+
import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
|
|
19390
|
+
import { join as join41 } from "path";
|
|
18904
19391
|
var PeriscopeError = class extends Error {
|
|
18905
19392
|
};
|
|
18906
19393
|
var LivePeriscopeError = class extends PeriscopeError {
|
|
@@ -18910,22 +19397,22 @@ var LivePeriscopeError = class extends PeriscopeError {
|
|
|
18910
19397
|
this.data = { refused: "live_periscope_exists", ...live };
|
|
18911
19398
|
}
|
|
18912
19399
|
};
|
|
18913
|
-
var priorDir = (stateDir) =>
|
|
18914
|
-
var priorPath = (stateDir, ticketRecordId) =>
|
|
19400
|
+
var priorDir = (stateDir) => join41(stateDir, "periscope-priors");
|
|
19401
|
+
var priorPath = (stateDir, ticketRecordId) => join41(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
18915
19402
|
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
18916
|
-
|
|
19403
|
+
rmSync19(priorPath(stateDir, ticketRecordId), { force: true });
|
|
18917
19404
|
}
|
|
18918
19405
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
18919
19406
|
try {
|
|
18920
|
-
|
|
18921
|
-
|
|
19407
|
+
mkdirSync25(priorDir(stateDir), { recursive: true });
|
|
19408
|
+
writeFileSync29(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
18922
19409
|
} catch {
|
|
18923
19410
|
}
|
|
18924
19411
|
}
|
|
18925
19412
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
18926
19413
|
try {
|
|
18927
19414
|
const p = priorPath(stateDir, ticketRecordId);
|
|
18928
|
-
return existsSync39(p) ? JSON.parse(
|
|
19415
|
+
return existsSync39(p) ? JSON.parse(readFileSync35(p, "utf8")) : void 0;
|
|
18929
19416
|
} catch {
|
|
18930
19417
|
return void 0;
|
|
18931
19418
|
}
|
|
@@ -19322,14 +19809,14 @@ ${tail2(preview.output)}`);
|
|
|
19322
19809
|
}
|
|
19323
19810
|
|
|
19324
19811
|
// src/periscope-idle.ts
|
|
19325
|
-
import { execFileSync as
|
|
19812
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
19326
19813
|
import { statSync as statSync11 } from "fs";
|
|
19327
|
-
import { join as
|
|
19814
|
+
import { join as join42 } from "path";
|
|
19328
19815
|
var MAX_SCANNED_FILES = 2e4;
|
|
19329
19816
|
function newestWorktreeMtime(root) {
|
|
19330
19817
|
let listing;
|
|
19331
19818
|
try {
|
|
19332
|
-
listing =
|
|
19819
|
+
listing = execFileSync9("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
19333
19820
|
cwd: root,
|
|
19334
19821
|
encoding: "utf8",
|
|
19335
19822
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -19344,7 +19831,7 @@ function newestWorktreeMtime(root) {
|
|
|
19344
19831
|
if (!rel) continue;
|
|
19345
19832
|
if (++scanned > MAX_SCANNED_FILES) break;
|
|
19346
19833
|
try {
|
|
19347
|
-
const m = statSync11(
|
|
19834
|
+
const m = statSync11(join42(root, rel)).mtimeMs;
|
|
19348
19835
|
if (newest === null || m > newest) newest = m;
|
|
19349
19836
|
} catch {
|
|
19350
19837
|
}
|
|
@@ -19419,21 +19906,21 @@ async function runPeriscopeIdlePass(d) {
|
|
|
19419
19906
|
import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
19420
19907
|
|
|
19421
19908
|
// src/ssh-keys.ts
|
|
19422
|
-
import { execFileSync as
|
|
19423
|
-
import { existsSync as existsSync40, mkdirSync as
|
|
19424
|
-
import { dirname as dirname22, join as
|
|
19909
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
19910
|
+
import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
|
|
19911
|
+
import { dirname as dirname22, join as join43 } from "path";
|
|
19425
19912
|
var SshKeyError = class extends Error {
|
|
19426
19913
|
};
|
|
19427
19914
|
function shipSshKeyPathFor(stateDir) {
|
|
19428
|
-
return
|
|
19915
|
+
return join43(stateDir, "ssh", "id_ed25519");
|
|
19429
19916
|
}
|
|
19430
19917
|
function ensureShipSshKeypair(stateDir) {
|
|
19431
19918
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
19432
19919
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
19433
19920
|
if (!existsSync40(privateKeyPath)) {
|
|
19434
|
-
|
|
19921
|
+
mkdirSync26(dirname22(privateKeyPath), { recursive: true });
|
|
19435
19922
|
try {
|
|
19436
|
-
|
|
19923
|
+
execFileSync10("ssh-keygen", [
|
|
19437
19924
|
"-t",
|
|
19438
19925
|
"ed25519",
|
|
19439
19926
|
"-N",
|
|
@@ -19454,12 +19941,12 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
19454
19941
|
if (!existsSync40(publicKeyPath)) {
|
|
19455
19942
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
19456
19943
|
}
|
|
19457
|
-
const publicKey =
|
|
19944
|
+
const publicKey = readFileSync36(publicKeyPath, "utf8").trim();
|
|
19458
19945
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
19459
19946
|
}
|
|
19460
19947
|
function sshKeygenAvailable() {
|
|
19461
19948
|
try {
|
|
19462
|
-
|
|
19949
|
+
execFileSync10("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
19463
19950
|
return true;
|
|
19464
19951
|
} catch (e) {
|
|
19465
19952
|
return e.code !== "ENOENT";
|
|
@@ -19497,7 +19984,7 @@ function shipIdentityJson(s) {
|
|
|
19497
19984
|
}
|
|
19498
19985
|
|
|
19499
19986
|
// src/mcp-endpoint-registry.ts
|
|
19500
|
-
import { createHash as
|
|
19987
|
+
import { createHash as createHash9 } from "crypto";
|
|
19501
19988
|
function endpointsUrl(api, workspaceId, id) {
|
|
19502
19989
|
const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
|
|
19503
19990
|
return id ? `${base}/${id}` : base;
|
|
@@ -19567,7 +20054,7 @@ async function probeEndpointAccess(api, workspaceId) {
|
|
|
19567
20054
|
}
|
|
19568
20055
|
var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
|
|
19569
20056
|
function registrationKey(url, mcpSecret, controlUrl) {
|
|
19570
|
-
return
|
|
20057
|
+
return createHash9("sha256").update(`${url}
|
|
19571
20058
|
${mcpSecret ?? ""}
|
|
19572
20059
|
${controlUrl ?? ""}`).digest("hex").slice(0, 16);
|
|
19573
20060
|
}
|
|
@@ -19616,10 +20103,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
19616
20103
|
|
|
19617
20104
|
// src/passenger-sync-daemon.ts
|
|
19618
20105
|
import { createServer as createHttpServer } from "http";
|
|
19619
|
-
import { spawn as
|
|
20106
|
+
import { spawn as spawn8 } from "child_process";
|
|
19620
20107
|
import { randomBytes } from "crypto";
|
|
19621
|
-
import { existsSync as existsSync41, mkdirSync as
|
|
19622
|
-
import { join as
|
|
20108
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync27, readFileSync as readFileSync37, writeFileSync as writeFileSync30, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
|
|
20109
|
+
import { join as join44 } from "path";
|
|
19623
20110
|
|
|
19624
20111
|
// src/secret-compare.ts
|
|
19625
20112
|
import { timingSafeEqual } from "crypto";
|
|
@@ -19703,26 +20190,26 @@ function runSyncDaemonFromEnv(env = process.env) {
|
|
|
19703
20190
|
return server;
|
|
19704
20191
|
}
|
|
19705
20192
|
var defaultSpawn2 = (cmd, args, env) => {
|
|
19706
|
-
const child =
|
|
20193
|
+
const child = spawn8(cmd, args, { detached: true, stdio: "ignore", env });
|
|
19707
20194
|
child.unref();
|
|
19708
20195
|
return child;
|
|
19709
20196
|
};
|
|
19710
20197
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
19711
|
-
return
|
|
20198
|
+
return join44(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
19712
20199
|
}
|
|
19713
20200
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
19714
20201
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
19715
20202
|
if (!existsSync41(path)) return void 0;
|
|
19716
20203
|
try {
|
|
19717
|
-
return JSON.parse(
|
|
20204
|
+
return JSON.parse(readFileSync37(path, "utf8"));
|
|
19718
20205
|
} catch {
|
|
19719
20206
|
return void 0;
|
|
19720
20207
|
}
|
|
19721
20208
|
}
|
|
19722
20209
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
19723
20210
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
19724
|
-
|
|
19725
|
-
|
|
20211
|
+
mkdirSync27(join44(stateDir, "passengers"), { recursive: true });
|
|
20212
|
+
writeFileSync30(path, `${JSON.stringify(state, null, 2)}
|
|
19726
20213
|
`);
|
|
19727
20214
|
}
|
|
19728
20215
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -19742,7 +20229,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
19742
20229
|
}
|
|
19743
20230
|
}
|
|
19744
20231
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
19745
|
-
const dir =
|
|
20232
|
+
const dir = join44(stateDir, "passengers");
|
|
19746
20233
|
if (!existsSync41(dir)) return [];
|
|
19747
20234
|
const suffix = "-sync.json";
|
|
19748
20235
|
return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -19831,8 +20318,8 @@ function describeCause(err3) {
|
|
|
19831
20318
|
}
|
|
19832
20319
|
|
|
19833
20320
|
// src/daemon.ts
|
|
19834
|
-
import { existsSync as existsSync42, mkdirSync as
|
|
19835
|
-
import { join as
|
|
20321
|
+
import { existsSync as existsSync42, mkdirSync as mkdirSync28, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync12, writeFileSync as writeFileSync31 } from "fs";
|
|
20322
|
+
import { join as join45 } from "path";
|
|
19836
20323
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
19837
20324
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
19838
20325
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -19920,7 +20407,7 @@ function latestMtime(path) {
|
|
|
19920
20407
|
if (!stat2.isDirectory()) return 0;
|
|
19921
20408
|
let latest = stat2.mtimeMs;
|
|
19922
20409
|
for (const entry of readdirSync17(path, { withFileTypes: true })) {
|
|
19923
|
-
latest = Math.max(latest, latestMtime(
|
|
20410
|
+
latest = Math.max(latest, latestMtime(join45(path, entry.name)));
|
|
19924
20411
|
}
|
|
19925
20412
|
return latest;
|
|
19926
20413
|
}
|
|
@@ -19932,7 +20419,7 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
19932
20419
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync42(opts.reloadSentinel);
|
|
19933
20420
|
}
|
|
19934
20421
|
function reloadSentinelFor(stateDir) {
|
|
19935
|
-
return
|
|
20422
|
+
return join45(stateDir, "reload-requested");
|
|
19936
20423
|
}
|
|
19937
20424
|
function readReloadRequestedAt(stateDir) {
|
|
19938
20425
|
try {
|
|
@@ -19942,17 +20429,17 @@ function readReloadRequestedAt(stateDir) {
|
|
|
19942
20429
|
}
|
|
19943
20430
|
}
|
|
19944
20431
|
function daemonStampFor(stateDir) {
|
|
19945
|
-
return
|
|
20432
|
+
return join45(stateDir, "daemon.started");
|
|
19946
20433
|
}
|
|
19947
20434
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
19948
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
20435
|
+
mkdirSync28(stateDir, { recursive: true });
|
|
20436
|
+
rmSync20(reloadSentinelFor(stateDir), { force: true });
|
|
20437
|
+
writeFileSync31(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
19951
20438
|
`);
|
|
19952
20439
|
}
|
|
19953
20440
|
function readDaemonStarted(stateDir) {
|
|
19954
20441
|
try {
|
|
19955
|
-
const v = JSON.parse(
|
|
20442
|
+
const v = JSON.parse(readFileSync38(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
19956
20443
|
return typeof v === "number" ? v : null;
|
|
19957
20444
|
} catch {
|
|
19958
20445
|
return null;
|
|
@@ -20017,7 +20504,7 @@ async function runOnePassUnguarded(o) {
|
|
|
20017
20504
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
20018
20505
|
const emit = o.newEmitter();
|
|
20019
20506
|
const decide = o.decide ?? decideCycle;
|
|
20020
|
-
const
|
|
20507
|
+
const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
|
|
20021
20508
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
20022
20509
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
20023
20510
|
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
@@ -20121,7 +20608,7 @@ async function runOnePassUnguarded(o) {
|
|
|
20121
20608
|
settle = resolve7;
|
|
20122
20609
|
});
|
|
20123
20610
|
const ready = (tookTicket2) => settle(tookTicket2);
|
|
20124
|
-
const
|
|
20611
|
+
const run3 = spawn11(role, decision, ctx, { ready }).then(() => {
|
|
20125
20612
|
breaker.recordSuccess(role);
|
|
20126
20613
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
20127
20614
|
clearShipAttention(o.state, workspaceAuthKey(role));
|
|
@@ -20170,8 +20657,8 @@ async function runOnePassUnguarded(o) {
|
|
|
20170
20657
|
});
|
|
20171
20658
|
if (o.inFlight) {
|
|
20172
20659
|
const set = o.inFlight;
|
|
20173
|
-
set.add(
|
|
20174
|
-
void
|
|
20660
|
+
set.add(run3);
|
|
20661
|
+
void run3.finally(() => set.delete(run3));
|
|
20175
20662
|
}
|
|
20176
20663
|
const reported = TICKET_ROLES.has(role) ? await settled : true;
|
|
20177
20664
|
const tookTicket = reported !== false;
|
|
@@ -20507,7 +20994,7 @@ async function buildEnvironment(route2, ship, ticket) {
|
|
|
20507
20994
|
}
|
|
20508
20995
|
|
|
20509
20996
|
// src/control-api.ts
|
|
20510
|
-
import { createHash as
|
|
20997
|
+
import { createHash as createHash10 } from "crypto";
|
|
20511
20998
|
import {
|
|
20512
20999
|
chmodSync as chmodSync4,
|
|
20513
21000
|
closeSync as closeSync5,
|
|
@@ -20516,17 +21003,17 @@ import {
|
|
|
20516
21003
|
openSync as openSync5,
|
|
20517
21004
|
readSync as readSync3,
|
|
20518
21005
|
readdirSync as readdirSync18,
|
|
20519
|
-
rmSync as
|
|
21006
|
+
rmSync as rmSync21,
|
|
20520
21007
|
statSync as statSync13,
|
|
20521
21008
|
watch
|
|
20522
21009
|
} from "fs";
|
|
20523
21010
|
import { createConnection } from "net";
|
|
20524
21011
|
import { createServer } from "http";
|
|
20525
|
-
import { basename as
|
|
20526
|
-
import { execFile, spawn as
|
|
21012
|
+
import { basename as basename13, dirname as dirname23, join as join46 } from "path";
|
|
21013
|
+
import { execFile as execFile2, spawn as spawn10 } from "child_process";
|
|
20527
21014
|
|
|
20528
21015
|
// src/periscope-control.ts
|
|
20529
|
-
import { spawn as
|
|
21016
|
+
import { spawn as spawn9 } from "child_process";
|
|
20530
21017
|
var reply = (status2, body) => ({ status: status2, body });
|
|
20531
21018
|
var err = (status2, message) => reply(status2, { error: message });
|
|
20532
21019
|
var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
|
|
@@ -20717,7 +21204,7 @@ async function handlePeriscopes(h, q) {
|
|
|
20717
21204
|
}
|
|
20718
21205
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
20719
21206
|
return new Promise((resolve7) => {
|
|
20720
|
-
const child =
|
|
21207
|
+
const child = spawn9(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
20721
21208
|
let stdout = "";
|
|
20722
21209
|
let stderr = "";
|
|
20723
21210
|
let pending = "";
|
|
@@ -20751,7 +21238,7 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
20751
21238
|
// src/control-api.ts
|
|
20752
21239
|
var PAUSABLE = ["dev", "design", "qa"];
|
|
20753
21240
|
var CONTROL_SOCKET_NAME = "control.sock";
|
|
20754
|
-
var controlSocketFor = (stateDir) =>
|
|
21241
|
+
var controlSocketFor = (stateDir) => join46(stateDir, CONTROL_SOCKET_NAME);
|
|
20755
21242
|
var MAX_SOCKET_PATH = 100;
|
|
20756
21243
|
var MAX_TAIL_BYTES = 1024 * 1024;
|
|
20757
21244
|
var MAX_WAIT_SECONDS = 30;
|
|
@@ -20767,7 +21254,7 @@ var ControlAuth = class {
|
|
|
20767
21254
|
if (!token) return "unauthorized";
|
|
20768
21255
|
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
20769
21256
|
const now = (this.o.now ?? Date.now)();
|
|
20770
|
-
const id =
|
|
21257
|
+
const id = createHash10("sha256").update(token).digest("hex");
|
|
20771
21258
|
const hit = this.cache.get(id);
|
|
20772
21259
|
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
20773
21260
|
const verdict = await this.ask(token);
|
|
@@ -20788,7 +21275,7 @@ var ControlAuth = class {
|
|
|
20788
21275
|
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
20789
21276
|
for (const k of this.o.workspaceSecrets?.(workspaceId) ?? []) if (k && secretsMatch(token, k)) return "ok";
|
|
20790
21277
|
const now = (this.o.now ?? Date.now)();
|
|
20791
|
-
const id = `${workspaceId}:${
|
|
21278
|
+
const id = `${workspaceId}:${createHash10("sha256").update(token).digest("hex")}`;
|
|
20792
21279
|
const hit = this.cache.get(id);
|
|
20793
21280
|
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
20794
21281
|
const verdict = await this.ask(token, [workspaceId]);
|
|
@@ -20838,7 +21325,7 @@ function candidates(live, rotated) {
|
|
|
20838
21325
|
const out = [live];
|
|
20839
21326
|
if (!rotated) return out;
|
|
20840
21327
|
try {
|
|
20841
|
-
for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(
|
|
21328
|
+
for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname23(live), n));
|
|
20842
21329
|
} catch {
|
|
20843
21330
|
}
|
|
20844
21331
|
return out;
|
|
@@ -20961,7 +21448,7 @@ function pauseDoc(state) {
|
|
|
20961
21448
|
}
|
|
20962
21449
|
function describeStream(r) {
|
|
20963
21450
|
return {
|
|
20964
|
-
run:
|
|
21451
|
+
run: basename13(r.streamPath).replace(/\.jsonl$/, ""),
|
|
20965
21452
|
pid: r.pid,
|
|
20966
21453
|
ticket: r.ticket,
|
|
20967
21454
|
role: r.role,
|
|
@@ -21036,9 +21523,9 @@ async function route(deps, method, url, req, abort) {
|
|
|
21036
21523
|
if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
|
|
21037
21524
|
if (p === "/streams" && get2) return reply2(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
|
|
21038
21525
|
if (p.startsWith("/streams/") && get2) {
|
|
21039
|
-
const
|
|
21040
|
-
if (!RUN_NAME_RE.test(
|
|
21041
|
-
const file =
|
|
21526
|
+
const run3 = decodeURIComponent(p.slice("/streams/".length));
|
|
21527
|
+
if (!RUN_NAME_RE.test(run3) || run3.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run3)) return err2(400, "not a run name");
|
|
21528
|
+
const file = join46(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
|
|
21042
21529
|
if (!existsSync43(file)) return err2(404, "no such run");
|
|
21043
21530
|
return tailReply(file, url.searchParams, false, abort);
|
|
21044
21531
|
}
|
|
@@ -21101,7 +21588,7 @@ async function startControlServer(deps) {
|
|
|
21101
21588
|
log(`control API not started: socket path is too long for a unix socket (${path})`);
|
|
21102
21589
|
return null;
|
|
21103
21590
|
}
|
|
21104
|
-
if (await socketIsStale(path))
|
|
21591
|
+
if (await socketIsStale(path)) rmSync21(path, { force: true });
|
|
21105
21592
|
else if (existsSync43(path)) {
|
|
21106
21593
|
log(`control API not started: ${path} is in use by another process`);
|
|
21107
21594
|
return null;
|
|
@@ -21148,7 +21635,7 @@ async function startControlServer(deps) {
|
|
|
21148
21635
|
path,
|
|
21149
21636
|
close: () => new Promise((done) => {
|
|
21150
21637
|
server.close(() => {
|
|
21151
|
-
|
|
21638
|
+
rmSync21(path, { force: true });
|
|
21152
21639
|
done();
|
|
21153
21640
|
});
|
|
21154
21641
|
})
|
|
@@ -21157,25 +21644,25 @@ async function startControlServer(deps) {
|
|
|
21157
21644
|
});
|
|
21158
21645
|
}
|
|
21159
21646
|
var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
21160
|
-
function runDetached(commands, spawnFn =
|
|
21647
|
+
function runDetached(commands, spawnFn = spawn10) {
|
|
21161
21648
|
const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
|
|
21162
21649
|
const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
|
|
21163
21650
|
child.on("error", () => {
|
|
21164
21651
|
});
|
|
21165
21652
|
child.unref();
|
|
21166
21653
|
}
|
|
21167
|
-
function schedulerHandler(control,
|
|
21654
|
+
function schedulerHandler(control, run3 = runDetached) {
|
|
21168
21655
|
return (action) => {
|
|
21169
21656
|
const { startCommand: start, stopCommand: stop } = control;
|
|
21170
21657
|
if (!start || !stop) return { accepted: false, detail: "this host has no service manager for the daemon (cron or none)" };
|
|
21171
21658
|
const commands = action === "start" ? [start] : action === "stop" ? [stop] : [stop, start];
|
|
21172
|
-
|
|
21659
|
+
run3(commands);
|
|
21173
21660
|
return { accepted: true, detail: `${action} requested` };
|
|
21174
21661
|
};
|
|
21175
21662
|
}
|
|
21176
21663
|
function crewJson(args, timeoutMs = 6e4) {
|
|
21177
21664
|
return new Promise((resolve7) => {
|
|
21178
|
-
|
|
21665
|
+
execFile2(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
|
|
21179
21666
|
try {
|
|
21180
21667
|
const v = JSON.parse(String(stdout));
|
|
21181
21668
|
resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
@@ -21188,53 +21675,53 @@ function crewJson(args, timeoutMs = 6e4) {
|
|
|
21188
21675
|
|
|
21189
21676
|
// src/periscope-mcp.ts
|
|
21190
21677
|
import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
|
|
21191
|
-
import { basename as
|
|
21678
|
+
import { basename as basename14, dirname as dirname24, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
|
|
21192
21679
|
import { z } from "zod";
|
|
21193
21680
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21194
21681
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21195
21682
|
|
|
21196
21683
|
// src/periscope-endpoints.ts
|
|
21197
21684
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
21198
|
-
import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as
|
|
21199
|
-
import { join as
|
|
21200
|
-
var secretDir = (stateDir) =>
|
|
21201
|
-
var secretFile = (stateDir, id) =>
|
|
21202
|
-
var endpointDir = (stateDir) =>
|
|
21203
|
-
var endpointFile = (stateDir, id) =>
|
|
21685
|
+
import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync29, readdirSync as readdirSync19, readFileSync as readFileSync39, rmSync as rmSync22, writeFileSync as writeFileSync32 } from "fs";
|
|
21686
|
+
import { join as join47 } from "path";
|
|
21687
|
+
var secretDir = (stateDir) => join47(stateDir, "periscope-mcp");
|
|
21688
|
+
var secretFile = (stateDir, id) => join47(secretDir(stateDir), `${id}.json`);
|
|
21689
|
+
var endpointDir = (stateDir) => join47(stateDir, "periscope-endpoints");
|
|
21690
|
+
var endpointFile = (stateDir, id) => join47(endpointDir(stateDir), `${id}.json`);
|
|
21204
21691
|
function mintPeriscopeSecret(stateDir, ticketRecordId) {
|
|
21205
|
-
|
|
21692
|
+
mkdirSync29(secretDir(stateDir), { recursive: true, mode: 448 });
|
|
21206
21693
|
const secret = randomBytes3(32).toString("base64url");
|
|
21207
21694
|
const file = secretFile(stateDir, ticketRecordId);
|
|
21208
|
-
|
|
21695
|
+
writeFileSync32(file, `${JSON.stringify({ secret })}
|
|
21209
21696
|
`, { mode: 384 });
|
|
21210
21697
|
chmodSync5(file, 384);
|
|
21211
21698
|
return secret;
|
|
21212
21699
|
}
|
|
21213
21700
|
function readPeriscopeSecret(stateDir, ticketRecordId) {
|
|
21214
21701
|
try {
|
|
21215
|
-
const v = JSON.parse(
|
|
21702
|
+
const v = JSON.parse(readFileSync39(secretFile(stateDir, ticketRecordId), "utf8"));
|
|
21216
21703
|
return typeof v.secret === "string" && v.secret ? v.secret : void 0;
|
|
21217
21704
|
} catch {
|
|
21218
21705
|
return void 0;
|
|
21219
21706
|
}
|
|
21220
21707
|
}
|
|
21221
21708
|
function removePeriscopeSecret(stateDir, ticketRecordId) {
|
|
21222
|
-
|
|
21709
|
+
rmSync22(secretFile(stateDir, ticketRecordId), { force: true });
|
|
21223
21710
|
}
|
|
21224
21711
|
function readPeriscopeEndpoint(stateDir, ticketRecordId) {
|
|
21225
21712
|
try {
|
|
21226
|
-
return JSON.parse(
|
|
21713
|
+
return JSON.parse(readFileSync39(endpointFile(stateDir, ticketRecordId), "utf8"));
|
|
21227
21714
|
} catch {
|
|
21228
21715
|
return void 0;
|
|
21229
21716
|
}
|
|
21230
21717
|
}
|
|
21231
21718
|
function writePeriscopeEndpoint(stateDir, rec) {
|
|
21232
|
-
|
|
21233
|
-
|
|
21719
|
+
mkdirSync29(endpointDir(stateDir), { recursive: true });
|
|
21720
|
+
writeFileSync32(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
|
|
21234
21721
|
`);
|
|
21235
21722
|
}
|
|
21236
21723
|
function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
21237
|
-
|
|
21724
|
+
rmSync22(endpointFile(stateDir, ticketRecordId), { force: true });
|
|
21238
21725
|
}
|
|
21239
21726
|
function listPeriscopeEndpoints(stateDir) {
|
|
21240
21727
|
const dir = endpointDir(stateDir);
|
|
@@ -21331,7 +21818,7 @@ async function realAncestor(p) {
|
|
|
21331
21818
|
}
|
|
21332
21819
|
const parent = dirname24(cur);
|
|
21333
21820
|
if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
|
|
21334
|
-
tail3.push(
|
|
21821
|
+
tail3.push(basename14(cur));
|
|
21335
21822
|
cur = parent;
|
|
21336
21823
|
}
|
|
21337
21824
|
}
|
|
@@ -21341,13 +21828,13 @@ async function resolveInWorktree(root, requested) {
|
|
|
21341
21828
|
let abs;
|
|
21342
21829
|
if (isAbsolute3(requested)) {
|
|
21343
21830
|
abs = resolve5(requested);
|
|
21344
|
-
if (isInside(root, abs)) abs =
|
|
21831
|
+
if (isInside(root, abs)) abs = join48(realRoot, relative2(root, abs));
|
|
21345
21832
|
} else {
|
|
21346
21833
|
abs = resolve5(realRoot, requested);
|
|
21347
21834
|
}
|
|
21348
21835
|
if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
21349
21836
|
const { real, rest } = await realAncestor(abs);
|
|
21350
|
-
const final = rest ?
|
|
21837
|
+
const final = rest ? join48(real, rest) : real;
|
|
21351
21838
|
if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
21352
21839
|
if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
|
|
21353
21840
|
return final;
|
|
@@ -21415,7 +21902,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
21415
21902
|
const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
|
|
21416
21903
|
const lines2 = await Promise.all(entries.map(async (e) => {
|
|
21417
21904
|
if (e.isDirectory()) return `[DIR] ${e.name}`;
|
|
21418
|
-
const size = (await lstat(
|
|
21905
|
+
const size = (await lstat(join48(abs, e.name))).size;
|
|
21419
21906
|
return `[FILE] ${e.name} (${size} bytes)`;
|
|
21420
21907
|
}));
|
|
21421
21908
|
return text(lines2.join("\n") || "(empty)");
|
|
@@ -21481,8 +21968,8 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
21481
21968
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
21482
21969
|
}, async ({ path }) => {
|
|
21483
21970
|
const parent = await inTree(dirname24(path) === "" ? "." : dirname24(path));
|
|
21484
|
-
const abs =
|
|
21485
|
-
if (
|
|
21971
|
+
const abs = join48(parent, basename14(path));
|
|
21972
|
+
if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
|
|
21486
21973
|
const st = await lstat(abs).catch(() => void 0);
|
|
21487
21974
|
if (!st) return text(`${path} does not exist`, true);
|
|
21488
21975
|
if (st.isDirectory()) return text(`${path} is a directory; only files are deleted`, true);
|
|
@@ -21888,6 +22375,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
21888
22375
|
"rotate-passenger-url",
|
|
21889
22376
|
"run",
|
|
21890
22377
|
"sample-ticket",
|
|
22378
|
+
"sandbox",
|
|
21891
22379
|
"selftest",
|
|
21892
22380
|
"shell-init",
|
|
21893
22381
|
"ship",
|
|
@@ -21967,7 +22455,7 @@ if (command === "provision") {
|
|
|
21967
22455
|
process.exit(outcome.code);
|
|
21968
22456
|
}
|
|
21969
22457
|
if (command === "pool-member") {
|
|
21970
|
-
const pool = poolConfigFrom(argv, process.env, () =>
|
|
22458
|
+
const pool = poolConfigFrom(argv, process.env, () => readFileSync40(0, "utf8"));
|
|
21971
22459
|
if (typeof pool === "string") {
|
|
21972
22460
|
process.stderr.write(`crew pool-member: ${pool}
|
|
21973
22461
|
`);
|
|
@@ -22008,9 +22496,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
22008
22496
|
return m ? m[1] : null;
|
|
22009
22497
|
}, routeRelayUrl = function(route3) {
|
|
22010
22498
|
const ws = route3.resolved?.workspaceId;
|
|
22011
|
-
const
|
|
22012
|
-
if (!cfg.ship.relayHost || !
|
|
22013
|
-
return relayBaseUrlFor(
|
|
22499
|
+
const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
22500
|
+
if (!cfg.ship.relayHost || !slug2) return null;
|
|
22501
|
+
return relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
22014
22502
|
}, passengerApiKeys = function(routes) {
|
|
22015
22503
|
const keys = /* @__PURE__ */ new Map();
|
|
22016
22504
|
for (const r of routes) {
|
|
@@ -22075,8 +22563,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
22075
22563
|
stateDir: cfg.ship.stateDir,
|
|
22076
22564
|
shipName: cfg.ship.name,
|
|
22077
22565
|
apiFor: endpointApiFor(routes),
|
|
22078
|
-
urlFor: (
|
|
22079
|
-
controlUrlFor: (
|
|
22566
|
+
urlFor: (slug2) => publicUrlFor(slug2, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
|
|
22567
|
+
controlUrlFor: (slug2) => `${relayBaseUrlFor(slug2, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control`,
|
|
22080
22568
|
warn: (m) => emit.warn(m, { step: "passengers" })
|
|
22081
22569
|
};
|
|
22082
22570
|
}, releaseRefused = function() {
|
|
@@ -22096,8 +22584,20 @@ if (command === "passenger-sync-daemon") {
|
|
|
22096
22584
|
const out = refreshBaseBranch(r.dir, remote, base, true);
|
|
22097
22585
|
const unsafe = baseBranchUnsafe(out);
|
|
22098
22586
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
22587
|
+
}, sandboxTickDeps = function(emit2, c) {
|
|
22588
|
+
return {
|
|
22589
|
+
stateDir: cfg.ship.stateDir,
|
|
22590
|
+
run: spawnBuildRunner(),
|
|
22591
|
+
hasImage: engineHasImage(),
|
|
22592
|
+
removeImage: engineRemoveImage(),
|
|
22593
|
+
raise: (item) => {
|
|
22594
|
+
void raiseShipAttention({ state, emit: emit2, route: c, ship: cfg.ship }, item);
|
|
22595
|
+
},
|
|
22596
|
+
clear: (key) => clearShipAttention(state, key),
|
|
22597
|
+
log: (m) => emit2.emit(m)
|
|
22598
|
+
};
|
|
22099
22599
|
};
|
|
22100
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
22600
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth, sandboxTickDeps2 = sandboxTickDeps;
|
|
22101
22601
|
let cfg;
|
|
22102
22602
|
let firstRun;
|
|
22103
22603
|
let firstRunFromFlags = false;
|
|
@@ -22168,7 +22668,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22168
22668
|
throw e;
|
|
22169
22669
|
}
|
|
22170
22670
|
if (command === "credential") {
|
|
22171
|
-
const r = runCredential(positional[1],
|
|
22671
|
+
const r = runCredential(positional[1], readFileSync40(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
22172
22672
|
process.stdout.write(r.out);
|
|
22173
22673
|
process.stderr.write(r.err);
|
|
22174
22674
|
process.exit(r.code);
|
|
@@ -22180,9 +22680,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
22180
22680
|
forges: cfg.ship.forges,
|
|
22181
22681
|
stateDir: cfg.ship.stateDir,
|
|
22182
22682
|
configFile: cfg.configFile,
|
|
22183
|
-
readFile: (p) =>
|
|
22184
|
-
writeFile: (p, t) =>
|
|
22185
|
-
stdin: () =>
|
|
22683
|
+
readFile: (p) => readFileSync40(p, "utf8"),
|
|
22684
|
+
writeFile: (p, t) => writeFileSync33(p, t),
|
|
22685
|
+
stdin: () => readFileSync40(0, "utf8"),
|
|
22186
22686
|
env: process.env
|
|
22187
22687
|
}
|
|
22188
22688
|
);
|
|
@@ -22202,7 +22702,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22202
22702
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
22203
22703
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
22204
22704
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
22205
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
22705
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "sandbox" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
22206
22706
|
let route2;
|
|
22207
22707
|
try {
|
|
22208
22708
|
route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -22465,7 +22965,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22465
22965
|
}
|
|
22466
22966
|
if (host === "linux" && hasSystemd) {
|
|
22467
22967
|
const label = labelFor(CREW_HOME, "run");
|
|
22468
|
-
const staleTimer =
|
|
22968
|
+
const staleTimer = join49(homedir11(), ".config", "systemd", "user", `${label}.timer`);
|
|
22469
22969
|
if (existsSync45(staleTimer)) {
|
|
22470
22970
|
process.stdout.write(
|
|
22471
22971
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -22528,7 +23028,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22528
23028
|
for (const a of plan.actions) {
|
|
22529
23029
|
if (a.kind === "enable") {
|
|
22530
23030
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
22531
|
-
|
|
23031
|
+
writeFileSync33(cfg.configFile, planEnable(readFileSync40(cfg.configFile, "utf8"), a.route));
|
|
22532
23032
|
enabledSound.add(a.route);
|
|
22533
23033
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
22534
23034
|
} else {
|
|
@@ -22547,6 +23047,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
22547
23047
|
async function resolvedRepos(c) {
|
|
22548
23048
|
return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
|
|
22549
23049
|
}
|
|
23050
|
+
async function sandboxReposOf(routes) {
|
|
23051
|
+
const out = /* @__PURE__ */ new Map();
|
|
23052
|
+
for (const c of routes) {
|
|
23053
|
+
const { settled } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
23054
|
+
for (const r of settled) if (r.config.sandbox && !out.has(r.name)) out.set(r.name, { name: r.name, decl: r.config.sandbox });
|
|
23055
|
+
}
|
|
23056
|
+
return [...out.values()];
|
|
23057
|
+
}
|
|
22550
23058
|
async function resolveOneRepo(c, t) {
|
|
22551
23059
|
{
|
|
22552
23060
|
const id = repoIdForName(c, t.name);
|
|
@@ -23651,7 +24159,7 @@ ${cu.tail.trimEnd()}
|
|
|
23651
24159
|
process.exit(1);
|
|
23652
24160
|
}
|
|
23653
24161
|
requireArmed("run the daemon loop");
|
|
23654
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
24162
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join49(CREW_HOME, "src"), join49(CREW_HOME, "dist"), join49(CREW_HOME, "bin")];
|
|
23655
24163
|
const configPaths = daemonConfigPaths(cfg);
|
|
23656
24164
|
watchPaths.push(...configPaths);
|
|
23657
24165
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -23717,6 +24225,7 @@ ${cu.tail.trimEnd()}
|
|
|
23717
24225
|
});
|
|
23718
24226
|
if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
|
|
23719
24227
|
const inFlight = /* @__PURE__ */ new Set();
|
|
24228
|
+
let sandboxBuilding = false;
|
|
23720
24229
|
const stopReason = await runDaemonLoop({
|
|
23721
24230
|
inFlight,
|
|
23722
24231
|
onStaleDeferred: ({ runs, waitedMs }) => emit.warn(
|
|
@@ -23762,6 +24271,12 @@ ${cu.tail.trimEnd()}
|
|
|
23762
24271
|
),
|
|
23763
24272
|
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
23764
24273
|
onPass: (result) => {
|
|
24274
|
+
if (!sandboxBuilding) {
|
|
24275
|
+
sandboxBuilding = true;
|
|
24276
|
+
void sandboxReposOf(cfg.routes.filter((r) => r.enabled)).then((repos) => repos.length ? sandboxBuildTick(repos, sandboxTickDeps(emit, route2)) : null).catch((err3) => emit.warn(`sandbox image build tick failed: ${err3 instanceof Error ? err3.message : String(err3)}`)).finally(() => {
|
|
24277
|
+
sandboxBuilding = false;
|
|
24278
|
+
});
|
|
24279
|
+
}
|
|
23765
24280
|
if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
|
|
23766
24281
|
for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
|
|
23767
24282
|
}
|
|
@@ -23864,9 +24379,9 @@ ${cu.tail.trimEnd()}
|
|
|
23864
24379
|
`);
|
|
23865
24380
|
process.exit(2);
|
|
23866
24381
|
}
|
|
23867
|
-
let
|
|
24382
|
+
let run3;
|
|
23868
24383
|
try {
|
|
23869
|
-
|
|
24384
|
+
run3 = await runAgentsSync(target, {
|
|
23870
24385
|
userAgent: cfg.ship.userAgent,
|
|
23871
24386
|
stateDir: cfg.ship.stateDir,
|
|
23872
24387
|
dryRun,
|
|
@@ -23884,7 +24399,7 @@ ${cu.tail.trimEnd()}
|
|
|
23884
24399
|
}
|
|
23885
24400
|
throw e;
|
|
23886
24401
|
}
|
|
23887
|
-
const { personaResult, skillResult } =
|
|
24402
|
+
const { personaResult, skillResult } = run3;
|
|
23888
24403
|
if (flag("revoke-seat-keys")) {
|
|
23889
24404
|
for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
|
|
23890
24405
|
process.stdout.write(`${describeRevokeOutcome(o)}
|
|
@@ -23905,7 +24420,7 @@ ${cu.tail.trimEnd()}
|
|
|
23905
24420
|
}
|
|
23906
24421
|
}
|
|
23907
24422
|
}
|
|
23908
|
-
if (
|
|
24423
|
+
if (run3.diverged) process.exitCode = 1;
|
|
23909
24424
|
break;
|
|
23910
24425
|
}
|
|
23911
24426
|
case "skills": {
|
|
@@ -23947,10 +24462,10 @@ ${cu.tail.trimEnd()}
|
|
|
23947
24462
|
`);
|
|
23948
24463
|
if (!dryRun) {
|
|
23949
24464
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
23950
|
-
const raw = existsSync45(resolvedPath) ? JSON.parse(
|
|
24465
|
+
const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync40(resolvedPath, "utf8")) : {};
|
|
23951
24466
|
raw.agentSkills = result.agentSkills;
|
|
23952
|
-
|
|
23953
|
-
|
|
24467
|
+
mkdirSync30(dirOf2(resolvedPath), { recursive: true });
|
|
24468
|
+
writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
23954
24469
|
`);
|
|
23955
24470
|
}
|
|
23956
24471
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -23973,7 +24488,7 @@ ${cu.tail.trimEnd()}
|
|
|
23973
24488
|
throw e;
|
|
23974
24489
|
}
|
|
23975
24490
|
const oldName = cfg.ship.name;
|
|
23976
|
-
|
|
24491
|
+
writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), newName));
|
|
23977
24492
|
cfg.ship.name = newName;
|
|
23978
24493
|
let renameMachineId;
|
|
23979
24494
|
try {
|
|
@@ -24020,7 +24535,7 @@ ${cu.tail.trimEnd()}
|
|
|
24020
24535
|
process.exit(2);
|
|
24021
24536
|
};
|
|
24022
24537
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
24023
|
-
const configText =
|
|
24538
|
+
const configText = readFileSync40(cfg.configFile, "utf8");
|
|
24024
24539
|
if (sub === "check") {
|
|
24025
24540
|
try {
|
|
24026
24541
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -24060,7 +24575,7 @@ ${cu.tail.trimEnd()}
|
|
|
24060
24575
|
}
|
|
24061
24576
|
}, read: (r) => {
|
|
24062
24577
|
try {
|
|
24063
|
-
return
|
|
24578
|
+
return readFileSync40(resolvePath(dir, r), "utf8");
|
|
24064
24579
|
} catch {
|
|
24065
24580
|
return void 0;
|
|
24066
24581
|
}
|
|
@@ -24102,19 +24617,19 @@ ${cu.tail.trimEnd()}
|
|
|
24102
24617
|
let cleared;
|
|
24103
24618
|
if (clear2) {
|
|
24104
24619
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
24105
|
-
if (plan.removed.length > 0)
|
|
24620
|
+
if (plan.removed.length > 0) writeFileSync33(cfg.configFile, plan.text);
|
|
24106
24621
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
24107
24622
|
}
|
|
24108
24623
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
24109
24624
|
if (write === "route") {
|
|
24110
24625
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
24111
|
-
|
|
24626
|
+
writeFileSync33(cfg.configFile, plan.text);
|
|
24112
24627
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
24113
24628
|
} else if (write === "repo") {
|
|
24114
24629
|
const have = existingRepoConfig(files);
|
|
24115
24630
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
24116
24631
|
const file = resolvePath(dir, ".crew.yaml");
|
|
24117
|
-
|
|
24632
|
+
writeFileSync33(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
24118
24633
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
24119
24634
|
}
|
|
24120
24635
|
if (flag("json")) {
|
|
@@ -24211,7 +24726,7 @@ ${cu.tail.trimEnd()}
|
|
|
24211
24726
|
let resolvedRemotes;
|
|
24212
24727
|
if (existsSync45(resolvedFile)) {
|
|
24213
24728
|
try {
|
|
24214
|
-
resolvedRemotes = JSON.parse(
|
|
24729
|
+
resolvedRemotes = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
24215
24730
|
} catch {
|
|
24216
24731
|
}
|
|
24217
24732
|
}
|
|
@@ -24232,7 +24747,7 @@ ${cu.tail.trimEnd()}
|
|
|
24232
24747
|
isCheckout,
|
|
24233
24748
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
24234
24749
|
try {
|
|
24235
|
-
return Object.keys(parseRepoConfig(
|
|
24750
|
+
return Object.keys(parseRepoConfig(readFileSync40(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
24236
24751
|
} catch {
|
|
24237
24752
|
return false;
|
|
24238
24753
|
}
|
|
@@ -24281,7 +24796,7 @@ ${cu.tail.trimEnd()}
|
|
|
24281
24796
|
throw new ReposError(`${e.message}${authHint}
|
|
24282
24797
|
crew.yaml was not changed`);
|
|
24283
24798
|
}
|
|
24284
|
-
|
|
24799
|
+
writeFileSync33(cfg.configFile, plan2.text);
|
|
24285
24800
|
if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
|
|
24286
24801
|
`);
|
|
24287
24802
|
say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
|
|
@@ -24341,14 +24856,14 @@ ${cu.tail.trimEnd()}
|
|
|
24341
24856
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
24342
24857
|
`);
|
|
24343
24858
|
if (existsSync45(resolvedFile)) {
|
|
24344
|
-
const raw = JSON.parse(
|
|
24859
|
+
const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
24345
24860
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
24346
24861
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
24347
|
-
|
|
24862
|
+
writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24348
24863
|
`);
|
|
24349
24864
|
}
|
|
24350
24865
|
}
|
|
24351
|
-
|
|
24866
|
+
writeFileSync33(cfg.configFile, plan.text);
|
|
24352
24867
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
24353
24868
|
`);
|
|
24354
24869
|
} catch (e) {
|
|
@@ -24717,7 +25232,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24717
25232
|
process.exit(2);
|
|
24718
25233
|
}
|
|
24719
25234
|
if (found.shipNameAdopted && !dryRun) {
|
|
24720
|
-
|
|
25235
|
+
writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
24721
25236
|
cfg.ship.name = found.shipNameAdopted;
|
|
24722
25237
|
}
|
|
24723
25238
|
if (found.shipRow) {
|
|
@@ -24763,7 +25278,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24763
25278
|
const hadPreviousFile = existsSync45(resolvedPath);
|
|
24764
25279
|
if (hadPreviousFile) {
|
|
24765
25280
|
try {
|
|
24766
|
-
previous = JSON.parse(
|
|
25281
|
+
previous = JSON.parse(readFileSync40(resolvedPath, "utf8"));
|
|
24767
25282
|
} catch {
|
|
24768
25283
|
}
|
|
24769
25284
|
}
|
|
@@ -24893,12 +25408,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24893
25408
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
24894
25409
|
`);
|
|
24895
25410
|
} else {
|
|
24896
|
-
|
|
25411
|
+
mkdirSync30(dirname25(resolvedPath), { recursive: true });
|
|
24897
25412
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
24898
25413
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
24899
|
-
|
|
25414
|
+
writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
24900
25415
|
`);
|
|
24901
|
-
|
|
25416
|
+
renameSync6(tmpPath, resolvedPath);
|
|
24902
25417
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
24903
25418
|
`);
|
|
24904
25419
|
}
|
|
@@ -24942,8 +25457,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24942
25457
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
24943
25458
|
`);
|
|
24944
25459
|
} else {
|
|
24945
|
-
|
|
24946
|
-
|
|
25460
|
+
mkdirSync30(dirname25(apiKeyFile), { recursive: true });
|
|
25461
|
+
writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
24947
25462
|
`, { mode: 384 });
|
|
24948
25463
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
24949
25464
|
`);
|
|
@@ -25076,7 +25591,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25076
25591
|
if (syncRoute?.resolved) {
|
|
25077
25592
|
try {
|
|
25078
25593
|
process.stderr.write("\n");
|
|
25079
|
-
const
|
|
25594
|
+
const run3 = await runAgentsSync(syncRoute, {
|
|
25080
25595
|
userAgent: cfg.ship.userAgent,
|
|
25081
25596
|
stateDir: cfg.ship.stateDir,
|
|
25082
25597
|
say: (l) => process.stderr.write(` ${l}
|
|
@@ -25091,19 +25606,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25091
25606
|
if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role, p.tables)}
|
|
25092
25607
|
`);
|
|
25093
25608
|
}
|
|
25094
|
-
if (
|
|
25095
|
-
const missing =
|
|
25609
|
+
if (run3.readOnly) {
|
|
25610
|
+
const missing = run3.personaResult.outcomes.some((p) => p.action === "missing");
|
|
25096
25611
|
process.stderr.write(missing ? " A workspace admin has to open the Crew app or run `crew agents sync` once before this ship's seats can run.\n" : " Seats are linked to the workspace's existing personas.\n");
|
|
25097
25612
|
}
|
|
25098
|
-
connectEvents.emit("agents_synced",
|
|
25613
|
+
connectEvents.emit("agents_synced", run3.readOnly ? "agents linked (read-only key)" : "agents synced", {
|
|
25099
25614
|
route: route3,
|
|
25100
|
-
admin: !
|
|
25101
|
-
personas:
|
|
25102
|
-
skills:
|
|
25103
|
-
diverged:
|
|
25615
|
+
admin: !run3.readOnly,
|
|
25616
|
+
personas: run3.personaResult.outcomes,
|
|
25617
|
+
skills: run3.skillResult?.outcomes ?? [],
|
|
25618
|
+
diverged: run3.diverged
|
|
25104
25619
|
});
|
|
25105
25620
|
for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route3, ...m });
|
|
25106
|
-
if (
|
|
25621
|
+
if (run3.diverged) process.exitCode = 1;
|
|
25107
25622
|
} catch (e) {
|
|
25108
25623
|
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
|
|
25109
25624
|
else throw e;
|
|
@@ -25491,6 +26006,70 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25491
26006
|
if (!planIsUsable(planWt)) process.exitCode = 1;
|
|
25492
26007
|
break;
|
|
25493
26008
|
}
|
|
26009
|
+
case "sandbox": {
|
|
26010
|
+
const subS = positional[1];
|
|
26011
|
+
if (subS !== "build" && subS !== "images") {
|
|
26012
|
+
process.stderr.write("crew sandbox build [REPO] [--route ROUTE] [--json]\ncrew sandbox images [--route ROUTE] [--json]\n");
|
|
26013
|
+
process.exit(2);
|
|
26014
|
+
}
|
|
26015
|
+
const jsonS = flag("json");
|
|
26016
|
+
let routesS;
|
|
26017
|
+
try {
|
|
26018
|
+
routesS = value("route") ? [findRoute(cfg, value("route"))] : cfg.routes.filter((r) => r.enabled);
|
|
26019
|
+
} catch (e) {
|
|
26020
|
+
if (e instanceof ConfigError) {
|
|
26021
|
+
process.stderr.write(`crew: ${e.message}
|
|
26022
|
+
`);
|
|
26023
|
+
process.exit(2);
|
|
26024
|
+
}
|
|
26025
|
+
throw e;
|
|
26026
|
+
}
|
|
26027
|
+
const declared = await sandboxReposOf(routesS);
|
|
26028
|
+
if (subS === "images") {
|
|
26029
|
+
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
26030
|
+
const rows = declared.map((r) => {
|
|
26031
|
+
const plan = planSandbox(r.name, r.decl);
|
|
26032
|
+
return { repo: r.name, wants: plan.tag, tags: stateS[r.name]?.tags ?? [], ...sandboxImageStatus(stateS, plan) };
|
|
26033
|
+
});
|
|
26034
|
+
if (jsonS) {
|
|
26035
|
+
process.stdout.write(`${JSON.stringify(rows, null, 2)}
|
|
26036
|
+
`);
|
|
26037
|
+
break;
|
|
26038
|
+
}
|
|
26039
|
+
if (rows.length === 0) process.stdout.write("no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26040
|
+
for (const r of rows) {
|
|
26041
|
+
process.stdout.write(`${r.repo}: ${r.status} \u2014 ${r.detail}
|
|
26042
|
+
`);
|
|
26043
|
+
for (const t of r.tags) {
|
|
26044
|
+
const ageH = Math.max(0, Math.round((Date.now() - Date.parse(t.builtAt)) / 36e5));
|
|
26045
|
+
process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago)
|
|
26046
|
+
`);
|
|
26047
|
+
}
|
|
26048
|
+
}
|
|
26049
|
+
break;
|
|
26050
|
+
}
|
|
26051
|
+
const only = positional[2];
|
|
26052
|
+
const targets = only ? declared.filter((r) => r.name === only) : declared;
|
|
26053
|
+
if (targets.length === 0) {
|
|
26054
|
+
process.stderr.write(only ? `crew: no repository "${only}" declares a sandbox: section
|
|
26055
|
+
` : "crew: no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26056
|
+
process.exit(2);
|
|
26057
|
+
}
|
|
26058
|
+
const emitS = new Emitter({ route: routesS[0].route, eventFile: eventFileFor(cfg.ship.stateDir), rotateBytes: cfg.ship.eventsRotateBytes, logFile: cfg.ship.logFile, console: (l) => process.stderr.write(`${l}
|
|
26059
|
+
`) });
|
|
26060
|
+
const depsS = sandboxTickDeps(emitS, routesS[0]);
|
|
26061
|
+
let failed = false;
|
|
26062
|
+
for (const t of targets) {
|
|
26063
|
+
await sandboxBuildTick([t], depsS, true);
|
|
26064
|
+
const after = readSandboxState(cfg.ship.stateDir);
|
|
26065
|
+
const st = sandboxImageStatus(after, planSandbox(t.name, t.decl));
|
|
26066
|
+
if (st.status === "failed") failed = true;
|
|
26067
|
+
process.stdout.write(`${t.name}: ${st.status} \u2014 ${st.detail}
|
|
26068
|
+
`);
|
|
26069
|
+
}
|
|
26070
|
+
if (failed) process.exit(1);
|
|
26071
|
+
break;
|
|
26072
|
+
}
|
|
25494
26073
|
case "periscope": {
|
|
25495
26074
|
const subP = positional[1];
|
|
25496
26075
|
if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
|
|
@@ -25773,10 +26352,10 @@ ${where.join("\n")}`);
|
|
|
25773
26352
|
},
|
|
25774
26353
|
prepare: async (path) => {
|
|
25775
26354
|
for (const f of repoP.config.worktrees.copy) {
|
|
25776
|
-
const src =
|
|
26355
|
+
const src = join49(repoP.dir, f);
|
|
25777
26356
|
if (!existsSync45(src)) continue;
|
|
25778
|
-
|
|
25779
|
-
copyFileSync2(src,
|
|
26357
|
+
mkdirSync30(dirOf2(join49(path, f)), { recursive: true });
|
|
26358
|
+
copyFileSync2(src, join49(path, f));
|
|
25780
26359
|
}
|
|
25781
26360
|
const setup = repoP.config.hooks.setup;
|
|
25782
26361
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -26050,9 +26629,9 @@ ${where.join("\n")}`);
|
|
|
26050
26629
|
} };
|
|
26051
26630
|
const store = resetStore;
|
|
26052
26631
|
const h = hostFromUrl4(r.baseUrl);
|
|
26053
|
-
const [
|
|
26054
|
-
const session = await store.get(h,
|
|
26055
|
-
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h,
|
|
26632
|
+
const [slug2] = splitRoute(r.route);
|
|
26633
|
+
const session = await store.get(h, slug2);
|
|
26634
|
+
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug2) };
|
|
26056
26635
|
},
|
|
26057
26636
|
async hostSessions(stateDir) {
|
|
26058
26637
|
const store = resetStore;
|
|
@@ -26071,7 +26650,7 @@ ${where.join("\n")}`);
|
|
|
26071
26650
|
};
|
|
26072
26651
|
const droppedRoutes = [];
|
|
26073
26652
|
try {
|
|
26074
|
-
const rawCfg = parseYaml2(
|
|
26653
|
+
const rawCfg = parseYaml2(readFileSync40(cfg.configFile, "utf8"));
|
|
26075
26654
|
for (const r of rawCfg?.routes ?? []) {
|
|
26076
26655
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
26077
26656
|
try {
|
|
@@ -26124,8 +26703,8 @@ ${where.join("\n")}`);
|
|
|
26124
26703
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
26125
26704
|
break;
|
|
26126
26705
|
}
|
|
26127
|
-
|
|
26128
|
-
|
|
26706
|
+
mkdirSync30(cfg.ship.stateDir, { recursive: true });
|
|
26707
|
+
writeFileSync33(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
26129
26708
|
`);
|
|
26130
26709
|
process.stdout.write(
|
|
26131
26710
|
"crew reload: requested \u2014 the daemon relaunches with the new config once its running agents end (it starts no new work meanwhile); if it is busy, `crew reload --now` forces the relaunch and interrupts them\n"
|
|
@@ -26178,7 +26757,7 @@ ${where.join("\n")}`);
|
|
|
26178
26757
|
}
|
|
26179
26758
|
case "log": {
|
|
26180
26759
|
try {
|
|
26181
|
-
const text2 =
|
|
26760
|
+
const text2 = readFileSync40(cfg.ship.logFile, "utf8").split("\n");
|
|
26182
26761
|
process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
26183
26762
|
`);
|
|
26184
26763
|
} catch {
|
|
@@ -26201,7 +26780,7 @@ ${where.join("\n")}`);
|
|
|
26201
26780
|
};
|
|
26202
26781
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
26203
26782
|
const tracker = new Tracker(route2, cfg.ship);
|
|
26204
|
-
const configText =
|
|
26783
|
+
const configText = readFileSync40(cfg.configFile, "utf8");
|
|
26205
26784
|
const cfgBase = dirname25(cfg.configFile);
|
|
26206
26785
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
26207
26786
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
@@ -26230,7 +26809,7 @@ ${where.join("\n")}`);
|
|
|
26230
26809
|
}
|
|
26231
26810
|
if (configured2.some((r) => r.dir === dir)) {
|
|
26232
26811
|
try {
|
|
26233
|
-
|
|
26812
|
+
writeFileSync33(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
26234
26813
|
} catch (e) {
|
|
26235
26814
|
if (e instanceof ReposError) fail(e.message);
|
|
26236
26815
|
throw e;
|
|
@@ -26239,28 +26818,28 @@ ${where.join("\n")}`);
|
|
|
26239
26818
|
if (row) {
|
|
26240
26819
|
await tracker.deleteRepoRow(row.id);
|
|
26241
26820
|
if (existsSync45(resolvedFile)) {
|
|
26242
|
-
const raw = JSON.parse(
|
|
26821
|
+
const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
26243
26822
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
26244
26823
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
26245
|
-
|
|
26824
|
+
writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
26246
26825
|
`);
|
|
26247
26826
|
}
|
|
26248
26827
|
}
|
|
26249
|
-
if (existsSync45(
|
|
26828
|
+
if (existsSync45(join49(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
|
|
26250
26829
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
26251
26830
|
break;
|
|
26252
26831
|
}
|
|
26253
26832
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
26254
|
-
const fresh = !existsSync45(
|
|
26833
|
+
const fresh = !existsSync45(join49(dir, ".git"));
|
|
26255
26834
|
if (existsSync45(dir) && fresh && readdirSync20(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
26256
26835
|
if (dryRun) {
|
|
26257
26836
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
26258
26837
|
break;
|
|
26259
26838
|
}
|
|
26260
26839
|
if (fresh) {
|
|
26261
|
-
|
|
26262
|
-
|
|
26263
|
-
|
|
26840
|
+
mkdirSync30(dir, { recursive: true });
|
|
26841
|
+
writeFileSync33(join49(dir, "README.md"), SAMPLE_README);
|
|
26842
|
+
writeFileSync33(join49(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
26264
26843
|
const g = (...a) => {
|
|
26265
26844
|
const r = spawnSync10("git", a, { cwd: dir, encoding: "utf8" });
|
|
26266
26845
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -26279,16 +26858,16 @@ ${where.join("\n")}`);
|
|
|
26279
26858
|
});
|
|
26280
26859
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
26281
26860
|
if (existsSync45(resolvedFile)) {
|
|
26282
|
-
const raw = JSON.parse(
|
|
26861
|
+
const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
26283
26862
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
26284
26863
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
26285
|
-
|
|
26864
|
+
writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
26286
26865
|
`);
|
|
26287
26866
|
}
|
|
26288
26867
|
}
|
|
26289
26868
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
26290
26869
|
try {
|
|
26291
|
-
|
|
26870
|
+
writeFileSync33(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
26292
26871
|
} catch (e) {
|
|
26293
26872
|
if (e instanceof ReposError) fail(e.message);
|
|
26294
26873
|
throw e;
|
|
@@ -26523,7 +27102,7 @@ ${where.join("\n")}`);
|
|
|
26523
27102
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
26524
27103
|
// config-provenance display; this is the field that actually
|
|
26525
27104
|
// describes where a worktree lands by default.
|
|
26526
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
27105
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
26527
27106
|
worktreeParent: resolvePath(r.dir, "..")
|
|
26528
27107
|
})),
|
|
26529
27108
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -26731,7 +27310,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
26731
27310
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
26732
27311
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
26733
27312
|
try {
|
|
26734
|
-
return
|
|
27313
|
+
return readFileSync40(p, "utf8");
|
|
26735
27314
|
} catch {
|
|
26736
27315
|
return null;
|
|
26737
27316
|
}
|
|
@@ -26781,7 +27360,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
26781
27360
|
);
|
|
26782
27361
|
for (const r of await resolvedRepos(route2)) {
|
|
26783
27362
|
const { config } = r;
|
|
26784
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
27363
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename15(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
26785
27364
|
process.stdout.write(
|
|
26786
27365
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
26787
27366
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
@@ -27103,6 +27682,13 @@ ${foreign.join("\n")}
|
|
|
27103
27682
|
`);
|
|
27104
27683
|
}
|
|
27105
27684
|
}
|
|
27685
|
+
if (cfg.ship.runner === "sandbox") {
|
|
27686
|
+
const sb = checkSandboxRunner(cfg.ship.sandbox);
|
|
27687
|
+
process.stdout.write(`sandbox runner: ${sb.ok ? `OK (${sb.detail})` : `NOT READY \u2014 ${sb.detail}`}
|
|
27688
|
+
`);
|
|
27689
|
+
} else {
|
|
27690
|
+
process.stdout.write("sandbox runner: off (ship.runner is host)\n");
|
|
27691
|
+
}
|
|
27106
27692
|
const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
|
|
27107
27693
|
if (passengerRoutes.length === 0) {
|
|
27108
27694
|
process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
|
|
@@ -27146,13 +27732,13 @@ ${foreign.join("\n")}
|
|
|
27146
27732
|
if (cfg.ship.relayHost) {
|
|
27147
27733
|
for (const r of passengerRoutes) {
|
|
27148
27734
|
const ws = r.resolved?.workspaceId;
|
|
27149
|
-
const
|
|
27735
|
+
const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
27150
27736
|
if (!ws) continue;
|
|
27151
27737
|
process.stdout.write(
|
|
27152
|
-
` ${r.route} url: ${
|
|
27738
|
+
` ${r.route} url: ${slug2 ? publicUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain) : "not minted yet (first tunnel cycle creates it)"}
|
|
27153
27739
|
`
|
|
27154
27740
|
);
|
|
27155
|
-
const controlUrl =
|
|
27741
|
+
const controlUrl = slug2 && readPersistedTunnel(cfg.ship.stateDir, ws)?.controlSocket ? `${relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control` : void 0;
|
|
27156
27742
|
process.stdout.write(` ${r.route} control url: ${controlUrl ?? "none (the tunnel carries passengers only)"}
|
|
27157
27743
|
`);
|
|
27158
27744
|
const api = endpointApiFor([r])(ws);
|
|
@@ -27171,6 +27757,15 @@ ${foreign.join("\n")}
|
|
|
27171
27757
|
);
|
|
27172
27758
|
}
|
|
27173
27759
|
}
|
|
27760
|
+
{
|
|
27761
|
+
const declaredS = await sandboxReposOf(cfg.routes.filter((r) => r.enabled));
|
|
27762
|
+
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
27763
|
+
for (const r of declaredS) {
|
|
27764
|
+
const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl));
|
|
27765
|
+
process.stdout.write(`sandbox image: ${r.name}: ${st.status.toUpperCase()} \u2014 ${st.detail}
|
|
27766
|
+
`);
|
|
27767
|
+
}
|
|
27768
|
+
}
|
|
27174
27769
|
for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
|
|
27175
27770
|
const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
|
|
27176
27771
|
process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret}, attached only)
|
|
@@ -27229,3 +27824,4 @@ var endpointReconcileDeps2;
|
|
|
27229
27824
|
var releaseRefused2;
|
|
27230
27825
|
var doctorMachineId2;
|
|
27231
27826
|
var baseHealth2;
|
|
27827
|
+
var sandboxTickDeps2;
|