@tablation/crew 0.105.0 → 0.107.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 +1966 -1607
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1698,9 +1698,9 @@ var keychainSource = {
|
|
|
1698
1698
|
write(s, token, c) {
|
|
1699
1699
|
try {
|
|
1700
1700
|
if (process.platform === "darwin") {
|
|
1701
|
-
const
|
|
1701
|
+
const q2 = (v) => `"${v.replace(/(["\\])/g, "\\$1")}"`;
|
|
1702
1702
|
execFileSync("security", ["-i"], {
|
|
1703
|
-
input: `add-generic-password -U -s ${
|
|
1703
|
+
input: `add-generic-password -U -s ${q2(keychainService(s))} -a ${q2(c.host)} -w ${q2(token)}
|
|
1704
1704
|
`,
|
|
1705
1705
|
stdio: ["pipe", "ignore", "pipe"]
|
|
1706
1706
|
});
|
|
@@ -2185,6 +2185,12 @@ function parseSandbox(raw, file) {
|
|
|
2185
2185
|
const r = raw;
|
|
2186
2186
|
const packages = stringList(r.packages, "sandbox.packages", file);
|
|
2187
2187
|
const setup = stringList(r.setup, "sandbox.setup", file);
|
|
2188
|
+
const warm = stringList(r.warm, "sandbox.warm", file);
|
|
2189
|
+
for (const w of warm) {
|
|
2190
|
+
if (w.startsWith("/") || w.split("/").some((seg) => seg === ".." || seg === "" || seg === ".") || /[\\\s]/.test(w)) {
|
|
2191
|
+
throw new RepoConfigError(`${file}: sandbox.warm entry "${w}" must be a repo-relative path or glob with no "..", empty segments, backslashes or whitespace`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2188
2194
|
for (const p of packages) {
|
|
2189
2195
|
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
2196
|
}
|
|
@@ -2198,7 +2204,7 @@ function parseSandbox(raw, file) {
|
|
|
2198
2204
|
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
2205
|
}
|
|
2200
2206
|
}
|
|
2201
|
-
return { ...image ? { image } : {}, packages, setup };
|
|
2207
|
+
return { ...image ? { image } : {}, packages, setup, warm };
|
|
2202
2208
|
}
|
|
2203
2209
|
function parseRepoConfig(text2, file) {
|
|
2204
2210
|
let raw;
|
|
@@ -2218,7 +2224,7 @@ function parseRepoConfig(text2, file) {
|
|
|
2218
2224
|
rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
|
|
2219
2225
|
rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
|
|
2220
2226
|
rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
|
|
2221
|
-
rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup"]), "sandbox", file);
|
|
2227
|
+
rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup", "warm"]), "sandbox", file);
|
|
2222
2228
|
if (raw.version === void 0) throw new RepoConfigError(`${file}: version is required`);
|
|
2223
2229
|
if (raw.version !== SPEC_VERSION) {
|
|
2224
2230
|
throw new RepoConfigError(
|
|
@@ -2719,7 +2725,22 @@ function parseShipRunner(v) {
|
|
|
2719
2725
|
if (v === "host" || v === "sandbox") return v;
|
|
2720
2726
|
throw new ConfigError(`ship.runner must be "host" or "sandbox", got ${JSON.stringify(v)}`);
|
|
2721
2727
|
}
|
|
2722
|
-
var SANDBOX_KEYS = [
|
|
2728
|
+
var SANDBOX_KEYS = [
|
|
2729
|
+
"bin",
|
|
2730
|
+
"image",
|
|
2731
|
+
"policy",
|
|
2732
|
+
"cacheVolume",
|
|
2733
|
+
"mirrorDir",
|
|
2734
|
+
"workDir",
|
|
2735
|
+
"agentBin",
|
|
2736
|
+
"forgeTokenVar",
|
|
2737
|
+
"engine",
|
|
2738
|
+
"cacheWorkspace",
|
|
2739
|
+
"cacheImage",
|
|
2740
|
+
"depsImage",
|
|
2741
|
+
"depsCommand",
|
|
2742
|
+
"depsLockfile"
|
|
2743
|
+
];
|
|
2723
2744
|
function parseShipSandbox(v) {
|
|
2724
2745
|
if (v === void 0 || v === null) return void 0;
|
|
2725
2746
|
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.sandbox must be a map of settings");
|
|
@@ -5352,8 +5373,9 @@ function attentionProblems(items, already) {
|
|
|
5352
5373
|
// src/sandbox-image.ts
|
|
5353
5374
|
import { createHash as createHash5 } from "crypto";
|
|
5354
5375
|
import { spawn as spawn2 } from "child_process";
|
|
5376
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5355
5377
|
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";
|
|
5378
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
5357
5379
|
import { tmpdir as tmpdir2 } from "os";
|
|
5358
5380
|
|
|
5359
5381
|
// src/passenger-containers.ts
|
|
@@ -5613,7 +5635,25 @@ function crewLayer(base) {
|
|
|
5613
5635
|
].join("\n");
|
|
5614
5636
|
return { name: "crew", tag: `crew-sandbox-crew:${hash({ base: base.tag, dockerfile, files })}`, dockerfile, files };
|
|
5615
5637
|
}
|
|
5616
|
-
|
|
5638
|
+
var WARM_DIR = "/tmp/crew-warm";
|
|
5639
|
+
var WARM_MAX_BYTES = 64 * 1024 * 1024;
|
|
5640
|
+
var globToRegExp = (glob) => new RegExp(`^${glob.split("*").map((p) => p.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")}$`);
|
|
5641
|
+
function readWarmFiles(dir, ref, patterns) {
|
|
5642
|
+
if (patterns.length === 0) return {};
|
|
5643
|
+
const git2 = (args) => execFileSync4("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: WARM_MAX_BYTES, stdio: ["ignore", "pipe", "pipe"] });
|
|
5644
|
+
const all = git2(["ls-tree", "-r", "--name-only", ref]).split("\n").filter(Boolean);
|
|
5645
|
+
const res = patterns.map(globToRegExp);
|
|
5646
|
+
const out = {};
|
|
5647
|
+
let total = 0;
|
|
5648
|
+
for (const path of all.filter((f) => res.some((re) => re.test(f))).sort()) {
|
|
5649
|
+
const content = git2(["show", `${ref}:${path}`]);
|
|
5650
|
+
total += content.length;
|
|
5651
|
+
if (total > WARM_MAX_BYTES) throw new Error(`sandbox.warm files exceed ${WARM_MAX_BYTES} bytes`);
|
|
5652
|
+
out[path] = content;
|
|
5653
|
+
}
|
|
5654
|
+
return out;
|
|
5655
|
+
}
|
|
5656
|
+
function repoLayer(repo, decl, crew, warm) {
|
|
5617
5657
|
const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
5618
5658
|
const lines2 = [];
|
|
5619
5659
|
if (decl.image) {
|
|
@@ -5635,15 +5675,22 @@ function repoLayer(repo, decl, crew) {
|
|
|
5635
5675
|
`USER ${SANDBOX_UID}`,
|
|
5636
5676
|
"WORKDIR /sandbox"
|
|
5637
5677
|
);
|
|
5678
|
+
const files = {};
|
|
5679
|
+
const warming = Object.keys(warm).length > 0;
|
|
5680
|
+
if (warming) {
|
|
5681
|
+
for (const [path, content] of Object.entries(warm)) files[`warm/${path}`] = content;
|
|
5682
|
+
lines2.push(`COPY --chown=${SANDBOX_UID}:${SANDBOX_UID} warm/ ${WARM_DIR}/`, `WORKDIR ${WARM_DIR}`);
|
|
5683
|
+
}
|
|
5638
5684
|
for (const step of decl.setup) lines2.push(run(step));
|
|
5685
|
+
if (warming) lines2.push("WORKDIR /sandbox", run(`rm -rf ${WARM_DIR}`));
|
|
5639
5686
|
lines2.push(`LABEL io.crew.sandbox.layer="repo" io.crew.sandbox.repo=${JSON.stringify(repo)}`, "");
|
|
5640
5687
|
const dockerfile = lines2.join("\n");
|
|
5641
|
-
return { name: "repo", tag: `crew-sandbox/${slug(repo)}:${hash({ crew: crew.tag, dockerfile })}`, dockerfile, files
|
|
5688
|
+
return { name: "repo", tag: `crew-sandbox/${slug(repo)}:${hash({ crew: crew.tag, dockerfile, files })}`, dockerfile, files };
|
|
5642
5689
|
}
|
|
5643
|
-
function planSandbox(repo, decl) {
|
|
5690
|
+
function planSandbox(repo, decl, warm = {}) {
|
|
5644
5691
|
const base = baseLayer();
|
|
5645
5692
|
const crew = crewLayer(base);
|
|
5646
|
-
const layer = repoLayer(repo, decl, crew);
|
|
5693
|
+
const layer = repoLayer(repo, decl, crew, warm);
|
|
5647
5694
|
return { repo, tag: layer.tag, layers: [base, crew, layer] };
|
|
5648
5695
|
}
|
|
5649
5696
|
var sandboxStatePath = (stateDir) => join15(stateDir, "sandbox-images.json");
|
|
@@ -5718,7 +5765,10 @@ async function buildSandboxImage(plan, deps) {
|
|
|
5718
5765
|
mkdirSync9(ctx, { recursive: true });
|
|
5719
5766
|
try {
|
|
5720
5767
|
writeFileSync10(join15(ctx, "Dockerfile"), layer.dockerfile);
|
|
5721
|
-
for (const [name, content] of Object.entries(layer.files))
|
|
5768
|
+
for (const [name, content] of Object.entries(layer.files)) {
|
|
5769
|
+
mkdirSync9(dirname7(join15(ctx, name)), { recursive: true });
|
|
5770
|
+
writeFileSync10(join15(ctx, name), content);
|
|
5771
|
+
}
|
|
5722
5772
|
const code = await deps.run(["build", "--tag", layer.tag, "--file", join15(ctx, "Dockerfile"), ctx], logPath);
|
|
5723
5773
|
if (code !== 0) {
|
|
5724
5774
|
state[plan.repo] = { ...entry, failure: { tag: plan.tag, at: now().toISOString(), logPath } };
|
|
@@ -5749,7 +5799,7 @@ function sandboxFailedItem(repo, tag, layer, logPath, nowIso = (/* @__PURE__ */
|
|
|
5749
5799
|
async function sandboxBuildTick(repos, deps, force = false) {
|
|
5750
5800
|
const nowMs = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
5751
5801
|
for (const r of repos) {
|
|
5752
|
-
const plan = planSandbox(r.name, r.decl);
|
|
5802
|
+
const plan = planSandbox(r.name, r.decl, r.warm);
|
|
5753
5803
|
const state = readSandboxState(deps.stateDir)[r.name];
|
|
5754
5804
|
const upToDate = state?.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag);
|
|
5755
5805
|
if (upToDate) {
|
|
@@ -5772,10 +5822,10 @@ async function sandboxBuildTick(repos, deps, force = false) {
|
|
|
5772
5822
|
}
|
|
5773
5823
|
|
|
5774
5824
|
// src/doctor-checks.ts
|
|
5775
|
-
import { execFileSync as
|
|
5825
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
5776
5826
|
var PROBE_BRANCH = "crew-probe-branch";
|
|
5777
5827
|
var realGit = (cwd, args, env) => {
|
|
5778
|
-
|
|
5828
|
+
execFileSync5("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
|
|
5779
5829
|
};
|
|
5780
5830
|
function isSshRemote(url) {
|
|
5781
5831
|
return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
|
|
@@ -5897,16 +5947,16 @@ function roleCapWarnings(ship) {
|
|
|
5897
5947
|
|
|
5898
5948
|
// src/selftest.ts
|
|
5899
5949
|
import { spawn as spawn5 } from "child_process";
|
|
5900
|
-
import { existsSync as
|
|
5950
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, readFileSync as readFileSync22, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
|
|
5901
5951
|
import { tmpdir as tmpdir4 } from "os";
|
|
5902
|
-
import { join as
|
|
5952
|
+
import { join as join27 } from "path";
|
|
5903
5953
|
|
|
5904
5954
|
// src/agent.ts
|
|
5905
|
-
import { spawnSync as
|
|
5906
|
-
import { readFileSync as
|
|
5955
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
5956
|
+
import { readFileSync as readFileSync21, existsSync as existsSync24, statSync as statSync7, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
5907
5957
|
import { StringDecoder } from "string_decoder";
|
|
5908
5958
|
import { finished } from "stream/promises";
|
|
5909
|
-
import { join as
|
|
5959
|
+
import { join as join26, dirname as dirname9, basename as basename8 } from "path";
|
|
5910
5960
|
import { createHash as createHash6 } from "crypto";
|
|
5911
5961
|
|
|
5912
5962
|
// src/state.ts
|
|
@@ -7061,9 +7111,9 @@ async function addRunToTicket(target, t, recordId, run3) {
|
|
|
7061
7111
|
}
|
|
7062
7112
|
}
|
|
7063
7113
|
var queueDir = (stateDir) => join17(stateDir, "pending-agent-logs");
|
|
7064
|
-
function queueAgentRun(stateDir, name,
|
|
7114
|
+
function queueAgentRun(stateDir, name, q2) {
|
|
7065
7115
|
mkdirSync11(queueDir(stateDir), { recursive: true });
|
|
7066
|
-
writeFileSync12(join17(queueDir(stateDir), `${name}.json`), JSON.stringify(
|
|
7116
|
+
writeFileSync12(join17(queueDir(stateDir), `${name}.json`), JSON.stringify(q2));
|
|
7067
7117
|
}
|
|
7068
7118
|
async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
7069
7119
|
const dir = queueDir(stateDir);
|
|
@@ -7071,23 +7121,23 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
7071
7121
|
let sent = 0;
|
|
7072
7122
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7073
7123
|
const path = join17(dir, f);
|
|
7074
|
-
let
|
|
7124
|
+
let q2;
|
|
7075
7125
|
try {
|
|
7076
|
-
|
|
7126
|
+
q2 = JSON.parse(readFileSync16(path, "utf8"));
|
|
7077
7127
|
} catch {
|
|
7078
7128
|
continue;
|
|
7079
7129
|
}
|
|
7080
|
-
if (
|
|
7130
|
+
if (q2.version !== 1 || q2.route !== route2 || now.toISOString() < q2.nextAttemptAt) continue;
|
|
7081
7131
|
try {
|
|
7082
|
-
const apiKey = await keyFor(
|
|
7132
|
+
const apiKey = await keyFor(q2.role);
|
|
7083
7133
|
if (!apiKey) continue;
|
|
7084
|
-
const sentRun = await reportAgentRun({ ...
|
|
7134
|
+
const sentRun = await reportAgentRun({ ...q2.target, apiKey }, q2.report);
|
|
7085
7135
|
if (sentRun.counterWarning) log.warn(sentRun.counterWarning);
|
|
7086
7136
|
if (sentRun.counterRetry) queueTicketCounters(stateDir, f.replace(/\.json$/, ""), {
|
|
7087
7137
|
version: 1,
|
|
7088
|
-
route:
|
|
7089
|
-
role:
|
|
7090
|
-
target:
|
|
7138
|
+
route: q2.route,
|
|
7139
|
+
role: q2.role,
|
|
7140
|
+
target: q2.target,
|
|
7091
7141
|
write: sentRun.counterRetry,
|
|
7092
7142
|
attempts: 0,
|
|
7093
7143
|
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -7101,22 +7151,22 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
7101
7151
|
log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
|
|
7102
7152
|
continue;
|
|
7103
7153
|
}
|
|
7104
|
-
const attempts =
|
|
7154
|
+
const attempts = q2.attempts + 1;
|
|
7105
7155
|
writeFileSync12(path, JSON.stringify({
|
|
7106
|
-
...
|
|
7156
|
+
...q2,
|
|
7107
7157
|
attempts,
|
|
7108
7158
|
warned: true,
|
|
7109
7159
|
nextAttemptAt: new Date(now.getTime() + sleepBackoff(attempts)).toISOString()
|
|
7110
7160
|
}));
|
|
7111
|
-
if (!
|
|
7161
|
+
if (!q2.warned) log.warn(`a queued Agent Log report could not be sent yet (${e.message}); will retry`);
|
|
7112
7162
|
}
|
|
7113
7163
|
}
|
|
7114
7164
|
return sent;
|
|
7115
7165
|
}
|
|
7116
7166
|
var countersDir = (stateDir) => join17(stateDir, "pending-ticket-counters");
|
|
7117
|
-
function queueTicketCounters(stateDir, name,
|
|
7167
|
+
function queueTicketCounters(stateDir, name, q2) {
|
|
7118
7168
|
mkdirSync11(countersDir(stateDir), { recursive: true });
|
|
7119
|
-
writeFileSync12(join17(countersDir(stateDir), `${name}.json`), JSON.stringify(
|
|
7169
|
+
writeFileSync12(join17(countersDir(stateDir), `${name}.json`), JSON.stringify(q2));
|
|
7120
7170
|
}
|
|
7121
7171
|
async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
7122
7172
|
const dir = countersDir(stateDir);
|
|
@@ -7124,34 +7174,34 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
7124
7174
|
let sent = 0;
|
|
7125
7175
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7126
7176
|
const path = join17(dir, f);
|
|
7127
|
-
let
|
|
7177
|
+
let q2;
|
|
7128
7178
|
try {
|
|
7129
|
-
|
|
7179
|
+
q2 = JSON.parse(readFileSync16(path, "utf8"));
|
|
7130
7180
|
} catch {
|
|
7131
7181
|
continue;
|
|
7132
7182
|
}
|
|
7133
|
-
if (
|
|
7183
|
+
if (q2.version !== 1 || q2.route !== route2 || now.toISOString() < q2.nextAttemptAt) continue;
|
|
7134
7184
|
try {
|
|
7135
|
-
const apiKey = await keyFor(
|
|
7185
|
+
const apiKey = await keyFor(q2.role);
|
|
7136
7186
|
if (!apiKey) continue;
|
|
7137
|
-
await writeCounters({ ...
|
|
7187
|
+
await writeCounters({ ...q2.target, apiKey }, q2.write);
|
|
7138
7188
|
rmSync11(path, { force: true });
|
|
7139
7189
|
sent += 1;
|
|
7140
|
-
log.emit(`added a queued run's spend to ${
|
|
7190
|
+
log.emit(`added a queued run's spend to ${q2.write.ticketReference ?? q2.write.recordId} (${f})`);
|
|
7141
7191
|
} catch (e) {
|
|
7142
7192
|
if (!isRetryable(e)) {
|
|
7143
7193
|
rmSync11(path, { force: true });
|
|
7144
7194
|
log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
|
|
7145
7195
|
continue;
|
|
7146
7196
|
}
|
|
7147
|
-
const attempts =
|
|
7197
|
+
const attempts = q2.attempts + 1;
|
|
7148
7198
|
writeFileSync12(path, JSON.stringify({
|
|
7149
|
-
...
|
|
7199
|
+
...q2,
|
|
7150
7200
|
attempts,
|
|
7151
7201
|
warned: true,
|
|
7152
7202
|
nextAttemptAt: new Date(now.getTime() + sleepBackoff(attempts)).toISOString()
|
|
7153
7203
|
}));
|
|
7154
|
-
if (!
|
|
7204
|
+
if (!q2.warned) log.warn(`a queued ticket counter update could not be sent yet (${e.message}); will retry`);
|
|
7155
7205
|
}
|
|
7156
7206
|
}
|
|
7157
7207
|
return sent;
|
|
@@ -8516,15 +8566,15 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
8516
8566
|
|
|
8517
8567
|
// src/run-worktree.ts
|
|
8518
8568
|
import { existsSync as existsSync17 } from "fs";
|
|
8519
|
-
import { dirname as
|
|
8569
|
+
import { dirname as dirname8, join as join19 } from "path";
|
|
8520
8570
|
|
|
8521
8571
|
// src/ports.ts
|
|
8522
|
-
import { execFileSync as
|
|
8572
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
8523
8573
|
import { existsSync as existsSync16 } from "fs";
|
|
8524
8574
|
import { join as join18 } from "path";
|
|
8525
8575
|
var run2 = (cmd, args) => {
|
|
8526
8576
|
try {
|
|
8527
|
-
return
|
|
8577
|
+
return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8528
8578
|
} catch {
|
|
8529
8579
|
return "";
|
|
8530
8580
|
}
|
|
@@ -8665,7 +8715,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
8665
8715
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
8666
8716
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
8667
8717
|
}
|
|
8668
|
-
const path = join19(
|
|
8718
|
+
const path = join19(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
|
|
8669
8719
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
8670
8720
|
return { action: "cut-remote", path, branch: located.branch };
|
|
8671
8721
|
}
|
|
@@ -9936,11 +9986,11 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
|
9936
9986
|
}
|
|
9937
9987
|
|
|
9938
9988
|
// src/runner.ts
|
|
9939
|
-
import { spawn as spawn3, execFile, execFileSync as
|
|
9989
|
+
import { spawn as spawn3, execFile as execFile2, execFileSync as execFileSync7 } from "child_process";
|
|
9940
9990
|
import { mkdtempSync, writeFileSync as writeFileSync14, rmSync as rmSync13 } from "fs";
|
|
9941
9991
|
import { tmpdir as tmpdir3 } from "os";
|
|
9942
|
-
import { basename as basename7, join as
|
|
9943
|
-
import { promisify } from "util";
|
|
9992
|
+
import { basename as basename7, join as join25 } from "path";
|
|
9993
|
+
import { promisify as promisify2 } from "util";
|
|
9944
9994
|
|
|
9945
9995
|
// src/run-limit.ts
|
|
9946
9996
|
var DEFAULT_MAX_RUN_MINUTES = 240;
|
|
@@ -9991,1216 +10041,1712 @@ function runKilledComment(b) {
|
|
|
9991
10041
|
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.`;
|
|
9992
10042
|
}
|
|
9993
10043
|
|
|
9994
|
-
// src/
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10044
|
+
// src/sandbox-cache.ts
|
|
10045
|
+
import { execFile } from "child_process";
|
|
10046
|
+
import { promisify } from "util";
|
|
10047
|
+
|
|
10048
|
+
// src/deprovision-host.ts
|
|
10049
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
10050
|
+
import { existsSync as existsSync23, readdirSync as readdirSync11 } from "fs";
|
|
10051
|
+
|
|
10052
|
+
// src/interrupted.ts
|
|
10053
|
+
import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
|
|
10054
|
+
import { join as join23 } from "path";
|
|
10055
|
+
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
10056
|
+
function interruptedComment(b) {
|
|
10057
|
+
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.";
|
|
10058
|
+
return `${INTERRUPTED_MARKER}
|
|
10059
|
+
|
|
10060
|
+
The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
|
|
10061
|
+
}
|
|
10062
|
+
var handled = /* @__PURE__ */ new Set();
|
|
10063
|
+
var pidAlive = (pid) => {
|
|
10064
|
+
try {
|
|
10065
|
+
process.kill(pid, 0);
|
|
10066
|
+
return true;
|
|
10067
|
+
} catch (e) {
|
|
10068
|
+
return e.code === "EPERM";
|
|
10069
|
+
}
|
|
10000
10070
|
};
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
env: o.env,
|
|
10009
|
-
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10010
|
-
detached: true
|
|
10011
|
-
});
|
|
10012
|
-
return { child, handle: { runner: "host", pid: child.pid } };
|
|
10071
|
+
function runFinished(streamPath) {
|
|
10072
|
+
const path = `${streamPath}.meta.json`;
|
|
10073
|
+
if (!existsSync21(path)) return false;
|
|
10074
|
+
try {
|
|
10075
|
+
return JSON.parse(readFileSync20(path, "utf8")).running !== true;
|
|
10076
|
+
} catch {
|
|
10077
|
+
return true;
|
|
10013
10078
|
}
|
|
10014
|
-
|
|
10015
|
-
|
|
10079
|
+
}
|
|
10080
|
+
function orphanedStreams(o) {
|
|
10081
|
+
const dir = join23(o.stateDir, "streams");
|
|
10082
|
+
if (!existsSync21(dir)) return [];
|
|
10083
|
+
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
10084
|
+
const out = [];
|
|
10085
|
+
for (const name of readdirSync9(dir)) {
|
|
10086
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
10087
|
+
const streamPath = join23(dir, name);
|
|
10088
|
+
if (runFinished(streamPath)) continue;
|
|
10089
|
+
const sidecar = readRunSidecar(streamPath);
|
|
10090
|
+
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
10091
|
+
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
10092
|
+
out.push({ streamPath, sidecar });
|
|
10016
10093
|
}
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10094
|
+
return out;
|
|
10095
|
+
}
|
|
10096
|
+
async function reconcileRuns(o) {
|
|
10097
|
+
let adopted = 0;
|
|
10098
|
+
let interrupted = 0;
|
|
10099
|
+
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
10100
|
+
if (handled.has(streamPath)) continue;
|
|
10101
|
+
handled.add(streamPath);
|
|
10102
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
10103
|
+
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
|
|
10104
|
+
const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
|
|
10105
|
+
const work = (async () => {
|
|
10106
|
+
try {
|
|
10107
|
+
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
10108
|
+
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
10109
|
+
isAlive: o.isAlive,
|
|
10110
|
+
pollMs: o.pollMs,
|
|
10111
|
+
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
10112
|
+
});
|
|
10113
|
+
if (result.adopted) adopted++;
|
|
10114
|
+
else interrupted++;
|
|
10115
|
+
try {
|
|
10116
|
+
await o.afterRun?.(sidecar, result);
|
|
10117
|
+
} catch (e) {
|
|
10118
|
+
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
10119
|
+
}
|
|
10120
|
+
} catch (e) {
|
|
10121
|
+
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
10122
|
+
} finally {
|
|
10123
|
+
if (lock?.ok) lock.release();
|
|
10124
|
+
if (ticketLock?.ok) ticketLock.release();
|
|
10125
|
+
}
|
|
10126
|
+
})();
|
|
10127
|
+
if (isAlive(sidecar)) {
|
|
10128
|
+
if (o.inFlight) {
|
|
10129
|
+
o.inFlight.add(work);
|
|
10130
|
+
void work.finally(() => o.inFlight.delete(work));
|
|
10131
|
+
}
|
|
10132
|
+
} else {
|
|
10133
|
+
await work;
|
|
10024
10134
|
}
|
|
10025
10135
|
}
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10136
|
+
return { adopted, interrupted };
|
|
10137
|
+
}
|
|
10138
|
+
|
|
10139
|
+
// src/live-runs.ts
|
|
10140
|
+
import { existsSync as existsSync22, readdirSync as readdirSync10 } from "fs";
|
|
10141
|
+
import { join as join24 } from "path";
|
|
10142
|
+
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
10143
|
+
const dir = join24(stateDir, "streams");
|
|
10144
|
+
if (!existsSync22(dir)) return [];
|
|
10145
|
+
const out = [];
|
|
10146
|
+
for (const name of readdirSync10(dir)) {
|
|
10147
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
10148
|
+
const streamPath = join24(dir, name);
|
|
10149
|
+
if (runFinished(streamPath)) continue;
|
|
10150
|
+
const sidecar = readRunSidecar(streamPath);
|
|
10151
|
+
if (!sidecar || !isAlive(sidecar)) continue;
|
|
10152
|
+
out.push({ ...sidecar, streamPath });
|
|
10030
10153
|
}
|
|
10031
|
-
|
|
10032
|
-
function shQuote2(s) {
|
|
10033
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10154
|
+
return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
|
|
10034
10155
|
}
|
|
10035
|
-
function
|
|
10036
|
-
|
|
10037
|
-
return `crew-${base.slice(0, 36)}-${now.toString(36)}`.replace(/-+/g, "-");
|
|
10156
|
+
function siblingRuns(runs, ownTicketRecordId) {
|
|
10157
|
+
return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
|
|
10038
10158
|
}
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
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 };
|
|
10159
|
+
async function settleRunEnd(o) {
|
|
10160
|
+
const { tracker, emit } = o;
|
|
10161
|
+
const all = o.runs ?? liveRuns(o.stateDir);
|
|
10162
|
+
const slug2 = routeSlug(o.route.route);
|
|
10163
|
+
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug2), o.ownTicketRecordId);
|
|
10164
|
+
const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
|
|
10165
|
+
const newest = (rs) => rs[rs.length - 1];
|
|
10166
|
+
if (o.memberId) {
|
|
10081
10167
|
try {
|
|
10082
|
-
|
|
10083
|
-
|
|
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 } : {} };
|
|
10168
|
+
if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
|
|
10169
|
+
else await tracker.setCrewStatus(o.memberId, "idle");
|
|
10109
10170
|
} catch (e) {
|
|
10110
|
-
|
|
10111
|
-
throw new Error(`could not prepare the sandbox ${name}: ${describeExecError(e)}`);
|
|
10171
|
+
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
10112
10172
|
}
|
|
10113
10173
|
}
|
|
10114
|
-
|
|
10115
|
-
if (
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
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 {
|
|
10174
|
+
try {
|
|
10175
|
+
if (shipSiblings.length) {
|
|
10176
|
+
const n = newest(shipSiblings);
|
|
10177
|
+
await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
|
|
10178
|
+
} else {
|
|
10179
|
+
await tracker.beatIdle(o.shipName);
|
|
10134
10180
|
}
|
|
10181
|
+
} catch (e) {
|
|
10182
|
+
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
10135
10183
|
}
|
|
10136
|
-
};
|
|
10137
|
-
function describeExecError(e) {
|
|
10138
|
-
const err3 = e;
|
|
10139
|
-
return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
|
|
10140
10184
|
}
|
|
10141
|
-
function
|
|
10185
|
+
function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
|
|
10142
10186
|
try {
|
|
10143
|
-
return
|
|
10187
|
+
return worktreeForNumber(d, n)?.path ?? null;
|
|
10144
10188
|
} catch {
|
|
10145
10189
|
return null;
|
|
10146
10190
|
}
|
|
10191
|
+
}) {
|
|
10192
|
+
const ownRoute = run3.route === routeSlug(route2.route);
|
|
10193
|
+
const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
|
|
10194
|
+
const num = run3.ticket?.replace(/^\D+/, "") ?? "";
|
|
10195
|
+
return {
|
|
10196
|
+
role: run3.role,
|
|
10197
|
+
seat: ownRoute ? seatIds[run3.role] ?? null : null,
|
|
10198
|
+
ticket: run3.ticket,
|
|
10199
|
+
ticketRecordId: run3.ticketRecordId,
|
|
10200
|
+
repo: run3.repo ?? null,
|
|
10201
|
+
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
10202
|
+
stream: run3.streamPath,
|
|
10203
|
+
pid: run3.pid,
|
|
10204
|
+
startedAt: run3.spawnedAt,
|
|
10205
|
+
deadline: run3.deadline ?? null
|
|
10206
|
+
};
|
|
10147
10207
|
}
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
}
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10208
|
+
|
|
10209
|
+
// src/seat-keys.ts
|
|
10210
|
+
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
10211
|
+
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
10212
|
+
function keyIsAdmin(route2) {
|
|
10213
|
+
const role = route2.resolved?.role;
|
|
10214
|
+
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
10215
|
+
}
|
|
10216
|
+
var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
|
|
10217
|
+
var MEMBER_NO_KEY = 'no agent key for this seat \u2014 run `crew agents sync` (needs the "Mint agent keys" capability on your role)';
|
|
10218
|
+
var FORBIDDEN_LINE = 'seat keys: not minted \u2014 your role does not grant "Mint agent keys"; ask a workspace admin to add it to your project role (or run sync with an admin key)';
|
|
10219
|
+
function seatKeySlug(workspaceSlug, agentId) {
|
|
10220
|
+
return `${workspaceSlug}#agent:${agentId}`;
|
|
10221
|
+
}
|
|
10222
|
+
function slugFor(route2, agentId) {
|
|
10223
|
+
const [workspaceSlug] = splitRoute(route2.route);
|
|
10224
|
+
return seatKeySlug(workspaceSlug, agentId);
|
|
10225
|
+
}
|
|
10226
|
+
function seatAgentId(route2, role) {
|
|
10227
|
+
return route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
10228
|
+
}
|
|
10229
|
+
async function resolveReleaseKey(route2, getStore = getSessionStore) {
|
|
10230
|
+
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
10231
|
+
const agentId = route2.resolved?.releaseAgentId;
|
|
10232
|
+
if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
10157
10233
|
try {
|
|
10158
|
-
|
|
10234
|
+
const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
|
|
10235
|
+
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10236
|
+
return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10159
10237
|
} catch (e) {
|
|
10160
|
-
|
|
10238
|
+
const reason = describeReadError(e);
|
|
10239
|
+
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
10161
10240
|
}
|
|
10241
|
+
}
|
|
10242
|
+
async function resolveSeatKey(route2, role, getStore = getSessionStore) {
|
|
10243
|
+
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
10244
|
+
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
10245
|
+
const agentId = seatAgentId(route2, role);
|
|
10246
|
+
if (!agentId) return ship(keyIsAdmin(route2) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
10162
10247
|
try {
|
|
10163
|
-
|
|
10248
|
+
const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
|
|
10249
|
+
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10250
|
+
return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10164
10251
|
} catch (e) {
|
|
10165
|
-
const
|
|
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
|
-
};
|
|
10252
|
+
const reason = describeReadError(e);
|
|
10253
|
+
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
10170
10254
|
}
|
|
10171
|
-
return { ok: true, detail: version };
|
|
10172
10255
|
}
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
return
|
|
10256
|
+
function safeStderrLine(e) {
|
|
10257
|
+
const err3 = e;
|
|
10258
|
+
const raw = typeof err3?.stderr === "string" && err3.stderr.trim() ? err3.stderr : typeof err3?.message === "string" ? err3.message : "";
|
|
10259
|
+
for (const line of raw.split("\n")) {
|
|
10260
|
+
const t = line.trim();
|
|
10261
|
+
if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
|
|
10262
|
+
return t.slice(0, 120);
|
|
10263
|
+
}
|
|
10264
|
+
return void 0;
|
|
10182
10265
|
}
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10266
|
+
function describeReadError(e) {
|
|
10267
|
+
const fsErr = e;
|
|
10268
|
+
if (typeof fsErr?.path === "string" && typeof fsErr.code === "string") return `key file store unreadable (${fsErr.code})`;
|
|
10269
|
+
const code = e?.code;
|
|
10270
|
+
const line = safeStderrLine(e);
|
|
10271
|
+
const what = typeof code === "number" ? `security exited ${code}` : typeof code === "string" ? `security failed: ${code}` : void 0;
|
|
10272
|
+
const detail = [what, line].filter(Boolean).join(what && line ? ": " : "");
|
|
10273
|
+
return detail ? `OS keychain unavailable (${detail})` : "OS keychain unavailable";
|
|
10188
10274
|
}
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
}
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10275
|
+
function keychainExitCode(reason) {
|
|
10276
|
+
const m = /security exited (\d+)/.exec(reason);
|
|
10277
|
+
return m ? Number(m[1]) : void 0;
|
|
10278
|
+
}
|
|
10279
|
+
function describeStoreError(e) {
|
|
10280
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
10281
|
+
if (/User interaction is not allowed/i.test(msg)) {
|
|
10282
|
+
return "keychain unavailable (User interaction is not allowed \u2014 run from a logged-in session, or `security unlock-keychain` first)";
|
|
10283
|
+
}
|
|
10284
|
+
return "keychain unavailable (could not write to the OS keychain)";
|
|
10285
|
+
}
|
|
10286
|
+
async function mintMissingSeatKeys(route2, o) {
|
|
10287
|
+
const out = [];
|
|
10288
|
+
if (!route2.resolved) return out;
|
|
10289
|
+
const resolved = route2.resolved;
|
|
10290
|
+
const doFetch = o.fetchImpl ?? fetch;
|
|
10291
|
+
const targets = [
|
|
10292
|
+
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
10293
|
+
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
10294
|
+
];
|
|
10295
|
+
let store;
|
|
10200
10296
|
try {
|
|
10201
|
-
|
|
10202
|
-
} catch {
|
|
10203
|
-
return
|
|
10297
|
+
store = (o.getStore ?? getSessionStore)();
|
|
10298
|
+
} catch (e) {
|
|
10299
|
+
return targets.map(({ role }) => ({
|
|
10300
|
+
role,
|
|
10301
|
+
action: "failed",
|
|
10302
|
+
detail: describeStoreError(e)
|
|
10303
|
+
}));
|
|
10204
10304
|
}
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10305
|
+
const host = hostFromUrl2(route2.baseUrl);
|
|
10306
|
+
const base = route2.baseUrl.replace(/\/+$/, "");
|
|
10307
|
+
if (!o.dryRun) {
|
|
10308
|
+
const probeSlug = seatKeySlug(splitRoute(route2.route)[0], "_probe");
|
|
10209
10309
|
try {
|
|
10210
|
-
|
|
10211
|
-
|
|
10310
|
+
await store.set(host, probeSlug, {
|
|
10311
|
+
apiKey: "probe",
|
|
10312
|
+
workspaceId: resolved.workspaceId,
|
|
10313
|
+
identityId: "_probe",
|
|
10314
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10315
|
+
});
|
|
10316
|
+
await store.clear(host, probeSlug);
|
|
10317
|
+
} catch (e) {
|
|
10318
|
+
const detail = `${describeStoreError(e)}; no keys minted`;
|
|
10319
|
+
return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
|
|
10320
|
+
}
|
|
10321
|
+
}
|
|
10322
|
+
for (const { role, agentId } of targets) {
|
|
10323
|
+
if (!agentId) {
|
|
10324
|
+
out.push({ role, action: "no_agent" });
|
|
10212
10325
|
continue;
|
|
10213
10326
|
}
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10327
|
+
const slug2 = slugFor(route2, agentId);
|
|
10328
|
+
try {
|
|
10329
|
+
if ((await store.get(host, slug2))?.apiKey) {
|
|
10330
|
+
out.push({ role, action: "already_stored" });
|
|
10331
|
+
continue;
|
|
10332
|
+
}
|
|
10333
|
+
if (o.dryRun) {
|
|
10334
|
+
out.push({ role, action: "minted", detail: "dry run" });
|
|
10335
|
+
continue;
|
|
10336
|
+
}
|
|
10337
|
+
const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
10338
|
+
method: "POST",
|
|
10339
|
+
headers: {
|
|
10340
|
+
Authorization: `Bearer ${resolveApiKey(route2)}`,
|
|
10341
|
+
"Content-Type": "application/json",
|
|
10342
|
+
"User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
|
|
10343
|
+
},
|
|
10344
|
+
body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
|
|
10345
|
+
});
|
|
10346
|
+
if (res.status === 403) {
|
|
10347
|
+
out.push({ role, action: "forbidden" });
|
|
10348
|
+
break;
|
|
10349
|
+
}
|
|
10350
|
+
if (!res.ok) {
|
|
10351
|
+
out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
|
|
10352
|
+
continue;
|
|
10353
|
+
}
|
|
10354
|
+
const body = await res.json();
|
|
10355
|
+
if (!body.key) {
|
|
10356
|
+
out.push({ role, action: "failed", detail: "response carried no key" });
|
|
10357
|
+
continue;
|
|
10358
|
+
}
|
|
10359
|
+
try {
|
|
10360
|
+
const session = {
|
|
10361
|
+
apiKey: body.key,
|
|
10362
|
+
workspaceId: resolved.workspaceId,
|
|
10363
|
+
identityId: agentId,
|
|
10364
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10365
|
+
...body.id ? { keyId: body.id } : {}
|
|
10366
|
+
};
|
|
10367
|
+
await store.set(host, slug2, session);
|
|
10368
|
+
} catch (e) {
|
|
10369
|
+
let revoked = false;
|
|
10370
|
+
if (body.id) {
|
|
10371
|
+
try {
|
|
10372
|
+
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
|
|
10373
|
+
method: "DELETE",
|
|
10374
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
10375
|
+
});
|
|
10376
|
+
revoked = del2.ok;
|
|
10377
|
+
} catch {
|
|
10378
|
+
revoked = false;
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
out.push({
|
|
10382
|
+
role,
|
|
10383
|
+
action: "failed",
|
|
10384
|
+
detail: revoked ? `minted but could not be stored (${describeStoreError(e)}); revoked` : `a key for ${role} is live on the server and not stored (${describeStoreError(e)}) \u2014 revoke it from that seat's Agents row`
|
|
10385
|
+
});
|
|
10386
|
+
continue;
|
|
10387
|
+
}
|
|
10388
|
+
out.push({ role, action: "minted" });
|
|
10389
|
+
} catch (e) {
|
|
10390
|
+
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
10220
10391
|
}
|
|
10221
10392
|
}
|
|
10222
|
-
return
|
|
10393
|
+
return out;
|
|
10223
10394
|
}
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10395
|
+
function describeMintOutcome(o) {
|
|
10396
|
+
switch (o.action) {
|
|
10397
|
+
case "minted":
|
|
10398
|
+
return `${o.role}: agent key minted${o.detail ? ` (${o.detail})` : ""}`;
|
|
10399
|
+
case "already_stored":
|
|
10400
|
+
return `${o.role}: agent key already stored`;
|
|
10401
|
+
case "no_agent":
|
|
10402
|
+
return `${o.role}: no linked Agents row \u2014 writes as the ship until \`crew agents sync\` links one`;
|
|
10403
|
+
case "forbidden":
|
|
10404
|
+
return FORBIDDEN_LINE;
|
|
10405
|
+
case "failed":
|
|
10406
|
+
return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
|
|
10229
10407
|
}
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
const
|
|
10233
|
-
|
|
10234
|
-
const
|
|
10235
|
-
const
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
}
|
|
10242
|
-
const common = usesCommon ? readFileSync20(commonPath, "utf8") : "";
|
|
10243
|
-
const brief = readFileSync20(briefPath, "utf8");
|
|
10244
|
-
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
10245
|
-
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
10246
|
-
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
10247
|
-
let expanded;
|
|
10408
|
+
}
|
|
10409
|
+
async function revokeSeatKeys(route2, o) {
|
|
10410
|
+
const out = [];
|
|
10411
|
+
if (!route2.resolved) return out;
|
|
10412
|
+
const resolved = route2.resolved;
|
|
10413
|
+
const doFetch = o.fetchImpl ?? fetch;
|
|
10414
|
+
const targets = [
|
|
10415
|
+
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
10416
|
+
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
10417
|
+
];
|
|
10418
|
+
let store;
|
|
10248
10419
|
try {
|
|
10249
|
-
|
|
10250
|
-
`${effectiveCommon}${effectiveBrief}`,
|
|
10251
|
-
(name) => o.skillOverrides?.[name] ?? localSkills.get(name)
|
|
10252
|
-
);
|
|
10420
|
+
store = (o.getStore ?? getSessionStore)();
|
|
10253
10421
|
} catch (e) {
|
|
10254
|
-
|
|
10255
|
-
throw e;
|
|
10422
|
+
return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
|
|
10256
10423
|
}
|
|
10257
|
-
const
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
});
|
|
10264
|
-
const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
|
|
10265
|
-
const streamsDir = join24(o.stateDir, "streams");
|
|
10266
|
-
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
10267
|
-
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
10268
|
-
const { model } = seat;
|
|
10269
|
-
let toolchain = seat.toolchain;
|
|
10270
|
-
if (toolchain.auth === "workspace") {
|
|
10271
|
-
if (!o.apiKey || o.seatKind === "ship") {
|
|
10272
|
-
throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, which needs the seat's own agent key \u2014 this seat would fall back to the ship key (run \`crew agents sync\`)`);
|
|
10424
|
+
const host = hostFromUrl2(route2.baseUrl);
|
|
10425
|
+
const base = route2.baseUrl.replace(/\/+$/, "");
|
|
10426
|
+
for (const { role, agentId } of targets) {
|
|
10427
|
+
if (!agentId) {
|
|
10428
|
+
out.push({ role, action: "no_agent" });
|
|
10429
|
+
continue;
|
|
10273
10430
|
}
|
|
10274
|
-
|
|
10275
|
-
|
|
10431
|
+
const slug2 = slugFor(route2, agentId);
|
|
10432
|
+
try {
|
|
10433
|
+
const session = await store.get(host, slug2);
|
|
10434
|
+
if (!session?.apiKey) {
|
|
10435
|
+
out.push({ role, action: "not_stored" });
|
|
10436
|
+
continue;
|
|
10437
|
+
}
|
|
10438
|
+
if (!session.keyId) {
|
|
10439
|
+
out.push({ role, action: "failed", detail: "no key id was recorded when this key was minted \u2014 revoke it from the seat's Agents row" });
|
|
10440
|
+
continue;
|
|
10441
|
+
}
|
|
10442
|
+
if (o.dryRun) {
|
|
10443
|
+
out.push({ role, action: "revoked", detail: "dry run" });
|
|
10444
|
+
continue;
|
|
10445
|
+
}
|
|
10446
|
+
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
|
|
10447
|
+
method: "DELETE",
|
|
10448
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
10449
|
+
});
|
|
10450
|
+
if (!del2.ok && del2.status !== 404) {
|
|
10451
|
+
out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
|
|
10452
|
+
continue;
|
|
10453
|
+
}
|
|
10454
|
+
await store.clear(host, slug2);
|
|
10455
|
+
out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
|
|
10456
|
+
} catch (e) {
|
|
10457
|
+
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
10276
10458
|
}
|
|
10277
|
-
toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
|
|
10278
10459
|
}
|
|
10279
|
-
|
|
10280
|
-
const invocation = adapter.invocation(toolchain, {
|
|
10281
|
-
role: o.role,
|
|
10282
|
-
model,
|
|
10283
|
-
// Skipped when there's no key to send: a route `crew connect` hasn't
|
|
10284
|
-
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
10285
|
-
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
10286
|
-
});
|
|
10287
|
-
const columns = resolveContract(o.route.contract).columns;
|
|
10288
|
-
const agentLog = o.route.resolved && o.apiKey ? {
|
|
10289
|
-
baseUrl: o.route.baseUrl,
|
|
10290
|
-
workspaceId: o.route.resolved.workspaceId,
|
|
10291
|
-
apiKey: o.apiKey,
|
|
10292
|
-
userAgent: o.ship.userAgent,
|
|
10293
|
-
agentId: o.resolvedAgentId,
|
|
10294
|
-
...o.route.resolved.models?.issues ? { tickets: {
|
|
10295
|
-
issuesModelId: o.route.resolved.models.issues,
|
|
10296
|
-
keyColumn: columns.key,
|
|
10297
|
-
tagColumn: columns.tag,
|
|
10298
|
-
counters: {
|
|
10299
|
-
agentRuns: columns.agentRuns,
|
|
10300
|
-
tokensIn: columns.tokensIn,
|
|
10301
|
-
tokensOut: columns.tokensOut,
|
|
10302
|
-
cacheReadTokens: columns.cacheReadTokens,
|
|
10303
|
-
cacheWriteTokens: columns.cacheWriteTokens,
|
|
10304
|
-
costUsd: columns.costUsd
|
|
10305
|
-
}
|
|
10306
|
-
} } : {}
|
|
10307
|
-
} : void 0;
|
|
10308
|
-
return {
|
|
10309
|
-
role: o.role,
|
|
10310
|
-
route: routeLabel,
|
|
10311
|
-
// The route's directory, which is a STARTING POINT and not the
|
|
10312
|
-
// ticket's repo: the session picks its own working directory even when
|
|
10313
|
-
// the runner already selected a ticket (`o.repo` is known then, and
|
|
10314
|
-
// only feeds the per-repo toolchain choice). Where an area spans
|
|
10315
|
-
// several repos this is whichever one `repos` happened to list first
|
|
10316
|
-
// (config.ts), and the digest's `repo` column is what actually places
|
|
10317
|
-
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
10318
|
-
cwd: o.route.dir,
|
|
10319
|
-
bin: invocation.bin,
|
|
10320
|
-
args: invocation.args,
|
|
10321
|
-
toolchain: toolchain.name,
|
|
10322
|
-
harness: toolchain.harness,
|
|
10323
|
-
workspaceAuth: toolchain.auth === "workspace",
|
|
10324
|
-
provider: toolchainProvider(toolchain),
|
|
10325
|
-
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
10326
|
-
...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
|
|
10327
|
-
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
10328
|
-
warnings: [...seat.warnings ?? [], ...expanded.warnings],
|
|
10329
|
-
prompt,
|
|
10330
|
-
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
10331
|
-
unsetEnv: adapter.unsetEnv,
|
|
10332
|
-
setEnv: {
|
|
10333
|
-
...o.apiKey ? { [API_KEY_VAR]: o.apiKey } : {},
|
|
10334
|
-
...adapter.runEnv(toolchain),
|
|
10335
|
-
// The operator's global SessionStart hook (pair-context-hook.sh) injects
|
|
10336
|
-
// the interactive Pair persona's prompt for any session it can't tell
|
|
10337
|
-
// apart from a person's own terminal. Every headless lane run this
|
|
10338
|
-
// planner produces carries a real ticket brief already (`prompt`
|
|
10339
|
-
// above) — it must never be replaced by the Pair persona with no
|
|
10340
|
-
// ticket attached, which silently no-ops and strands the ticket
|
|
10341
|
-
// in_progress (ISSUE-490, ISSUE-591). Setting this is what lets that
|
|
10342
|
-
// hook tell the two apart; it has nothing to do with the run itself.
|
|
10343
|
-
[CREW_LANE_ROLE_VAR]: o.role,
|
|
10344
|
-
// CREW-2783: the ship's forge credential — git helper config plus the
|
|
10345
|
-
// token as an env var for `gh` and friends. Empty without `forges:`.
|
|
10346
|
-
...forgeRunEnv(o.ship.forges, { crewCommand: crewCommandLine(), configFile: process.env.CREW_CONFIG, stateDir: o.stateDir })
|
|
10347
|
-
},
|
|
10348
|
-
digestAttached: digest !== void 0,
|
|
10349
|
-
digestAgeSeconds: ageSeconds,
|
|
10350
|
-
model,
|
|
10351
|
-
streamPath: join24(streamsDir, `${base}.jsonl`),
|
|
10352
|
-
eventsPath: join24(streamsDir, `${base}.events.jsonl`),
|
|
10353
|
-
ticket: o.ticket,
|
|
10354
|
-
ticketRecordId: o.ticketRecordId ?? null,
|
|
10355
|
-
repo: o.repo ?? null,
|
|
10356
|
-
routeName: o.route.route,
|
|
10357
|
-
agentLog,
|
|
10358
|
-
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
10359
|
-
promptSha
|
|
10360
|
-
};
|
|
10460
|
+
return out;
|
|
10361
10461
|
}
|
|
10362
|
-
function
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
`unset: ${p.unsetEnv.join(" ")}`,
|
|
10374
|
-
// The VALUE is deliberately absent: --dry-run output is pasted into
|
|
10375
|
-
// tickets and chat.
|
|
10376
|
-
`set: ${Object.keys(p.setEnv).join(" ") || "(nothing)"}`,
|
|
10377
|
-
`stream: ${p.streamPath}`
|
|
10378
|
-
].join("\n");
|
|
10462
|
+
function describeRevokeOutcome(o) {
|
|
10463
|
+
switch (o.action) {
|
|
10464
|
+
case "revoked":
|
|
10465
|
+
return `${o.role}: agent key revoked and cleared from the keychain${o.detail ? ` (${o.detail})` : ""}`;
|
|
10466
|
+
case "not_stored":
|
|
10467
|
+
return `${o.role}: no agent key stored on this ship`;
|
|
10468
|
+
case "no_agent":
|
|
10469
|
+
return `${o.role}: no linked Agents row`;
|
|
10470
|
+
case "failed":
|
|
10471
|
+
return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
|
|
10472
|
+
}
|
|
10379
10473
|
}
|
|
10380
|
-
|
|
10381
|
-
|
|
10474
|
+
async function canMintAgentKeys(route2, o = {}) {
|
|
10475
|
+
const resolved = route2.resolved;
|
|
10476
|
+
if (!resolved) return void 0;
|
|
10477
|
+
const agentId = seatAgentId(route2, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route2, r)).find(Boolean);
|
|
10478
|
+
if (!agentId) return void 0;
|
|
10382
10479
|
try {
|
|
10383
|
-
const
|
|
10384
|
-
|
|
10480
|
+
const res = await (o.fetchImpl ?? fetch)(`${route2.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
10481
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
10482
|
+
});
|
|
10483
|
+
if (res.ok) return true;
|
|
10484
|
+
return res.status === 403 ? false : void 0;
|
|
10385
10485
|
} catch {
|
|
10386
|
-
return
|
|
10486
|
+
return void 0;
|
|
10387
10487
|
}
|
|
10388
10488
|
}
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
const
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
return Math.abs(started - Date.parse(sc.spawnedAt)) < 15e3;
|
|
10489
|
+
|
|
10490
|
+
// src/deprovision-host.ts
|
|
10491
|
+
var DOCKER_LABEL_OWNER = "crew.owner=crew";
|
|
10492
|
+
var DOCKER_LABEL_CACHE = "crew.kind=cache";
|
|
10493
|
+
var DOCKER_LABEL_REPO_LAYER = "crew.layer=repo";
|
|
10494
|
+
function docker(args) {
|
|
10495
|
+
const res = spawnSync5("docker", args, { encoding: "utf8" });
|
|
10496
|
+
if (res.error || res.status !== 0) throw new Error(`docker ${args[0]} ${args[1] ?? ""} failed: ${(res.stderr || String(res.error ?? "")).trim().split("\n").pop()}`);
|
|
10497
|
+
return res.stdout;
|
|
10399
10498
|
}
|
|
10400
|
-
|
|
10499
|
+
var lines = (s) => s.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10500
|
+
function labeledDocker() {
|
|
10501
|
+
if (!dockerAvailable()) return { volumes: [], images: [] };
|
|
10401
10502
|
return {
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
ticket: plan.ticket ?? null,
|
|
10405
|
-
model: plan.model,
|
|
10406
|
-
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
10407
|
-
harness: adapterHarness,
|
|
10408
|
-
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER,
|
|
10409
|
-
eventsPath: plan.eventsPath,
|
|
10410
|
-
streamPath: plan.streamPath,
|
|
10411
|
-
agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
|
|
10412
|
-
promptVersion: plan.promptVersion,
|
|
10413
|
-
promptSha: plan.promptSha
|
|
10503
|
+
volumes: lines(docker(["volume", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_CACHE}`])),
|
|
10504
|
+
images: lines(docker(["image", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_REPO_LAYER}`]))
|
|
10414
10505
|
};
|
|
10415
10506
|
}
|
|
10416
|
-
|
|
10417
|
-
const
|
|
10418
|
-
const
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
}
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10507
|
+
function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
10508
|
+
const stateDir = cfg.ship.stateDir;
|
|
10509
|
+
const forgeHosts = () => Object.keys(cfg.ship.forges ?? {});
|
|
10510
|
+
const routeFor = (label) => cfg.routes.find((r) => routeSlug(r.route) === label);
|
|
10511
|
+
const sandboxes = () => dockerAvailable() ? listPassengerContainers().map((c) => c.name) : [];
|
|
10512
|
+
const unitPaths = () => {
|
|
10513
|
+
const host = hostPlatform();
|
|
10514
|
+
if (host === "windows") return [];
|
|
10515
|
+
const ctx = installContext();
|
|
10516
|
+
const systemd = detectSystemd();
|
|
10517
|
+
return ["run", "release", "passengers"].flatMap((job) => planInstall(cfg.ship, crewHome, host, systemd, job, ctx).unitPaths).filter((p) => existsSync23(p));
|
|
10518
|
+
};
|
|
10519
|
+
return {
|
|
10520
|
+
liveRuns: () => liveRuns(stateDir).map((r) => ({ role: r.role, route: r.route, pid: r.pid, ticketRecordId: r.ticketRecordId ?? void 0, streamPath: r.streamPath })),
|
|
10521
|
+
killRun: (run3) => killRunGroup(run3.pid),
|
|
10522
|
+
recordKill(run3) {
|
|
10523
|
+
emit.emit(`run_killed: ${run3.role} run on ${run3.route} stopped by crew deprovision`, {
|
|
10524
|
+
step: "agent",
|
|
10525
|
+
role: run3.role,
|
|
10526
|
+
data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
|
|
10527
|
+
});
|
|
10528
|
+
},
|
|
10529
|
+
async handBack(run3) {
|
|
10530
|
+
const route2 = routeFor(run3.route);
|
|
10531
|
+
if (!run3.ticketRecordId || !route2) return;
|
|
10532
|
+
const memberId = route2.resolved?.seats[run3.role];
|
|
10533
|
+
if (!memberId) return;
|
|
10534
|
+
const identity = await resolveSeatKey(route2, run3.role, getStore);
|
|
10535
|
+
const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
|
|
10536
|
+
const t = await tracker.ticket(run3.ticketRecordId);
|
|
10537
|
+
const target = repoTargetFor(route2, t.repo_id);
|
|
10538
|
+
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
10539
|
+
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
10540
|
+
let dirty = null;
|
|
10541
|
+
try {
|
|
10542
|
+
if (wt) dirty = status(wt.path).length > 0;
|
|
10543
|
+
} catch {
|
|
10544
|
+
}
|
|
10545
|
+
await tracker.postEvent(run3.ticketRecordId, interruptedComment({
|
|
10546
|
+
branch: wt?.branch ?? null,
|
|
10547
|
+
dirty,
|
|
10548
|
+
reason: "the host was released and wiped"
|
|
10549
|
+
}), memberId, "interrupted");
|
|
10550
|
+
},
|
|
10551
|
+
sandboxes,
|
|
10552
|
+
removeSandbox: (name) => {
|
|
10553
|
+
stopContainer(name);
|
|
10554
|
+
},
|
|
10555
|
+
async clearShipRows() {
|
|
10556
|
+
const out = [];
|
|
10557
|
+
for (const r of cfg.routes.filter((x) => x.resolved?.shipsModelId)) {
|
|
10558
|
+
try {
|
|
10559
|
+
const found = await new Tracker(r, cfg.ship).clearForRelease(cfg.ship.name);
|
|
10560
|
+
out.push({ route: r.route, ok: true, detail: found ? "cleared engaged, tunnel_status, relay_url" : "no Ships row for this ship" });
|
|
10561
|
+
} catch (e) {
|
|
10562
|
+
out.push({ route: r.route, ok: false, detail: e.message });
|
|
10563
|
+
}
|
|
10564
|
+
}
|
|
10565
|
+
return out;
|
|
10566
|
+
},
|
|
10567
|
+
forgeHosts,
|
|
10568
|
+
eraseForgeToken: (host) => eraseForgeToken(cfg.ship.forges, host, stateDir),
|
|
10569
|
+
labeled: labeledDocker,
|
|
10570
|
+
removeVolume: (name) => {
|
|
10571
|
+
docker(["volume", "rm", "-f", name]);
|
|
10572
|
+
},
|
|
10573
|
+
removeImage: (name) => {
|
|
10574
|
+
docker(["image", "rm", "-f", name]);
|
|
10575
|
+
},
|
|
10576
|
+
remaining() {
|
|
10577
|
+
const left = [];
|
|
10578
|
+
if (existsSync23(cfg.configFile)) left.push(`config ${cfg.configFile}`);
|
|
10579
|
+
for (const h of forgeHosts()) if (hasForgeToken(cfg.ship.forges, h, stateDir)) left.push(`forge token for ${h}`);
|
|
10580
|
+
try {
|
|
10581
|
+
for (const k of readdirSync11(keysDir(stateDir))) left.push(`seat key ${k}`);
|
|
10582
|
+
} catch {
|
|
10583
|
+
}
|
|
10584
|
+
for (const s of sandboxes()) left.push(`sandbox ${s}`);
|
|
10585
|
+
try {
|
|
10586
|
+
const { volumes, images } = labeledDocker();
|
|
10587
|
+
for (const v of volumes) left.push(`volume ${v}`);
|
|
10588
|
+
for (const i of images) left.push(`image ${i}`);
|
|
10589
|
+
} catch (e) {
|
|
10590
|
+
left.push(`docker could not be checked (${e.message})`);
|
|
10591
|
+
}
|
|
10592
|
+
for (const u of unitPaths()) left.push(`unit ${u}`);
|
|
10593
|
+
if (loadedCrewLabels().length) left.push(`loaded units ${loadedCrewLabels().join(", ")}`);
|
|
10594
|
+
return left;
|
|
10595
|
+
}
|
|
10596
|
+
};
|
|
10440
10597
|
}
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10598
|
+
|
|
10599
|
+
// src/sandbox-cache.ts
|
|
10600
|
+
var execFileP = promisify(execFile);
|
|
10601
|
+
var realExec2 = (bin, args, o) => execFileP(bin, args, { env: o?.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
|
|
10602
|
+
var OPENSHELL_ATTACHABLE_LABEL = "openshell.ai/sandbox-attachable=true";
|
|
10603
|
+
var OPENSHELL_WORKSPACE_LABEL = "openshell.ai/sandbox-attachable-workspace";
|
|
10604
|
+
var VOLUME_ROOT = "/cache";
|
|
10605
|
+
var DEFAULT_CACHE_SETTINGS = {
|
|
10606
|
+
engine: "docker",
|
|
10607
|
+
workspace: "crew",
|
|
10608
|
+
helperImage: "alpine/git:latest",
|
|
10609
|
+
depsLockfile: "pnpm-lock.yaml"
|
|
10610
|
+
};
|
|
10611
|
+
function parseRemote(url) {
|
|
10612
|
+
const u = url.trim();
|
|
10613
|
+
const m = /^[a-z+]+:\/\/(?:[^@/]*@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/i.exec(u) ?? /^(?:[\w.-]+@)?([\w.-]+):(?!\/\/)(.+?)(?:\.git)?\/?$/.exec(u);
|
|
10614
|
+
if (!m) return null;
|
|
10615
|
+
return { host: normalizeForgeHost(m[1]) || DEFAULT_FORGE_HOST, path: m[2].replace(/^\/+/, "") };
|
|
10616
|
+
}
|
|
10617
|
+
function mirrorRelPath(r) {
|
|
10618
|
+
return `mirrors/${r.host}/${r.path}.git`;
|
|
10619
|
+
}
|
|
10620
|
+
function depsRelDir(r) {
|
|
10621
|
+
return `cache/deps/${r.host}/${r.path}`;
|
|
10622
|
+
}
|
|
10623
|
+
var q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10624
|
+
function cloneScript(o) {
|
|
10625
|
+
const mirror = `${o.mirrorDir}/${mirrorRelPath(o.repo)}`;
|
|
10626
|
+
const depsRoot = `${o.mirrorDir}/${depsRelDir(o.repo)}`;
|
|
10627
|
+
return [
|
|
10628
|
+
`git clone --quiet --shared ${q(mirror)} ${q(o.workDir)}`,
|
|
10629
|
+
`cd ${q(o.workDir)}`,
|
|
10630
|
+
`git remote set-url origin ${q(o.origin)}`,
|
|
10631
|
+
`git checkout --quiet -b ${q(o.branch)}`,
|
|
10632
|
+
// A missing lockfile or tree is not an error: the run installs as it would without a cache.
|
|
10633
|
+
`{ if [ -f ${q(o.lockfile)} ]; then h=$(sha256sum ${q(o.lockfile)} | cut -c1-16); d=${q(depsRoot)}/$h; [ -d "$d" ] && cp -R -P "$d"/. . ; fi; true; }`
|
|
10634
|
+
].join(" && ");
|
|
10635
|
+
}
|
|
10636
|
+
var REFRESH_SCRIPT = `
|
|
10637
|
+
set -e
|
|
10638
|
+
m=${VOLUME_ROOT}/"$1"; url="$2"
|
|
10639
|
+
export GIT_TERMINAL_PROMPT=0
|
|
10640
|
+
git config --global --add safe.directory '*'
|
|
10641
|
+
if [ -n "$CREW_FORGE_TOKEN" ]; then
|
|
10642
|
+
H="Authorization: Basic $(printf '%s:%s' "$CREW_FORGE_USER" "$CREW_FORGE_TOKEN" | base64 | tr -d '\\n')"
|
|
10643
|
+
git() { command git -c "http.extraHeader=$H" "$@"; }
|
|
10644
|
+
fi
|
|
10645
|
+
if [ -d "$m" ]; then
|
|
10646
|
+
git -C "$m" fetch --prune --atomic --quiet origin
|
|
10647
|
+
else
|
|
10648
|
+
mkdir -p "$(dirname "$m")"; rm -rf "$m.tmp"
|
|
10649
|
+
git clone --mirror --quiet "$url" "$m.tmp"
|
|
10650
|
+
git -C "$m.tmp" config gc.auto 0
|
|
10651
|
+
mv "$m.tmp" "$m"
|
|
10652
|
+
fi
|
|
10653
|
+
date -u +%Y-%m-%dT%H:%M:%SZ > "$m/crew-last-refresh"
|
|
10654
|
+
`;
|
|
10655
|
+
var DEPS_SCRIPT = `
|
|
10656
|
+
set -e
|
|
10657
|
+
m=${VOLUME_ROOT}/"$1"; droot=${VOLUME_ROOT}/"$2"; lock="$3"
|
|
10658
|
+
export GIT_TERMINAL_PROMPT=0
|
|
10659
|
+
git config --global --add safe.directory '*'
|
|
10660
|
+
git -C "$m" cat-file -e HEAD:"$lock" 2>/dev/null || exit 0
|
|
10661
|
+
h=$(git -C "$m" show HEAD:"$lock" | sha256sum | cut -c1-16)
|
|
10662
|
+
[ -d "$droot/$h" ] && exit 0
|
|
10663
|
+
w=$(mktemp -d); trap 'rm -rf "$w"' EXIT
|
|
10664
|
+
git clone --quiet --shared "$m" "$w/src"
|
|
10665
|
+
cd "$w/src"
|
|
10666
|
+
sh -c "$CREW_DEPS_COMMAND"
|
|
10667
|
+
mkdir -p "$droot/$h.tmp"
|
|
10668
|
+
find . -name node_modules -type d -prune | while read -r p; do
|
|
10669
|
+
mkdir -p "$droot/$h.tmp/$(dirname "$p")"; cp -R -P "$p" "$droot/$h.tmp/$p"
|
|
10670
|
+
done
|
|
10671
|
+
rm -rf "$droot/$h"; mv "$droot/$h.tmp" "$droot/$h"
|
|
10672
|
+
`;
|
|
10673
|
+
var SandboxCache = class {
|
|
10674
|
+
s;
|
|
10675
|
+
exec;
|
|
10676
|
+
constructor(s, exec = realExec2) {
|
|
10677
|
+
this.s = s;
|
|
10678
|
+
this.exec = exec;
|
|
10679
|
+
}
|
|
10680
|
+
engine(args, env) {
|
|
10681
|
+
return this.exec(this.s.engine, args, { env });
|
|
10682
|
+
}
|
|
10683
|
+
/** Creates the volume with the labels OpenShell and `crew deprovision` look for, if it is not there. */
|
|
10684
|
+
async ensureVolume() {
|
|
10450
10685
|
try {
|
|
10451
|
-
|
|
10686
|
+
await this.engine(["volume", "inspect", this.s.volume]);
|
|
10687
|
+
return false;
|
|
10452
10688
|
} catch {
|
|
10453
|
-
return "";
|
|
10454
|
-
}
|
|
10455
|
-
try {
|
|
10456
|
-
let text2 = "";
|
|
10457
|
-
const chunk = Buffer.alloc(64 * 1024);
|
|
10458
|
-
for (; ; ) {
|
|
10459
|
-
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
10460
|
-
if (n <= 0) break;
|
|
10461
|
-
this.offset += n;
|
|
10462
|
-
text2 += this.decoder.write(chunk.subarray(0, n));
|
|
10463
|
-
}
|
|
10464
|
-
return text2;
|
|
10465
|
-
} finally {
|
|
10466
|
-
closeSync3(fd);
|
|
10467
10689
|
}
|
|
10690
|
+
await this.engine([
|
|
10691
|
+
"volume",
|
|
10692
|
+
"create",
|
|
10693
|
+
"--label",
|
|
10694
|
+
OPENSHELL_ATTACHABLE_LABEL,
|
|
10695
|
+
"--label",
|
|
10696
|
+
`${OPENSHELL_WORKSPACE_LABEL}=${this.s.workspace}`,
|
|
10697
|
+
"--label",
|
|
10698
|
+
DOCKER_LABEL_OWNER,
|
|
10699
|
+
"--label",
|
|
10700
|
+
DOCKER_LABEL_CACHE,
|
|
10701
|
+
this.s.volume
|
|
10702
|
+
]);
|
|
10703
|
+
return true;
|
|
10704
|
+
}
|
|
10705
|
+
helper(image, script, args, env = {}) {
|
|
10706
|
+
const flags = Object.keys(env).flatMap((k) => ["-e", k]);
|
|
10707
|
+
return this.engine(["run", "--rm", "--label", DOCKER_LABEL_OWNER, "-v", `${this.s.volume}:${VOLUME_ROOT}`, ...flags, "--entrypoint", "sh", image, "-c", script, "sh", ...args], { ...process.env, ...env });
|
|
10468
10708
|
}
|
|
10469
|
-
};
|
|
10470
|
-
var TAIL_POLL_MS = 200;
|
|
10471
|
-
var RunObserver = class {
|
|
10472
|
-
cycles = [];
|
|
10473
|
-
attribution = new TicketAttributionTracker();
|
|
10474
|
-
/** Hand-off writes and their outcomes, to tell a run whose hand-off never landed (CREW-2363). */
|
|
10475
|
-
handoff = new HandoffTranscriptTracker();
|
|
10476
|
-
result;
|
|
10477
|
-
/** Prefix tokens of the first assistant call that carried usage; kept once. */
|
|
10478
|
-
prefixTokens;
|
|
10479
|
-
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
10480
|
-
sawResult = false;
|
|
10481
|
-
muted = false;
|
|
10482
10709
|
/**
|
|
10483
|
-
*
|
|
10484
|
-
*
|
|
10710
|
+
* Refreshes every mirror (and, where configured, its dependency tree). One
|
|
10711
|
+
* repository failing does not stop the others; the caller raises attention
|
|
10712
|
+
* for the failures and the runs keep using the previous mirror state.
|
|
10485
10713
|
*/
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10714
|
+
async refresh(repos) {
|
|
10715
|
+
await this.ensureVolume();
|
|
10716
|
+
const out = [];
|
|
10717
|
+
for (const r of repos) {
|
|
10718
|
+
const env = r.token ? { CREW_FORGE_TOKEN: r.token, CREW_FORGE_USER: r.username ?? DEFAULT_FORGE_USERNAME } : {};
|
|
10719
|
+
try {
|
|
10720
|
+
await this.helper(this.s.helperImage, REFRESH_SCRIPT, [mirrorRelPath(r), forgeDescriptorFor(r.host).httpsUrl(r.host, r.path)], env);
|
|
10721
|
+
} catch (e) {
|
|
10722
|
+
out.push({ repo: r, ok: false, detail: describe(e) });
|
|
10723
|
+
continue;
|
|
10724
|
+
}
|
|
10725
|
+
if (this.s.depsCommand && this.s.depsImage) {
|
|
10726
|
+
try {
|
|
10727
|
+
await this.helper(this.s.depsImage, DEPS_SCRIPT, [mirrorRelPath(r), depsRelDir(r), this.s.depsLockfile], { CREW_DEPS_COMMAND: this.s.depsCommand });
|
|
10728
|
+
} catch (e) {
|
|
10729
|
+
out.push({ repo: r, ok: false, detail: `dependency tree: ${describe(e)}` });
|
|
10730
|
+
continue;
|
|
10731
|
+
}
|
|
10732
|
+
}
|
|
10733
|
+
out.push({ repo: r, ok: true });
|
|
10734
|
+
}
|
|
10735
|
+
return out;
|
|
10508
10736
|
}
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
this.buf = "";
|
|
10737
|
+
/** Mirrors in the volume with their last refresh; empty when the volume does not exist. */
|
|
10738
|
+
async list() {
|
|
10512
10739
|
try {
|
|
10513
|
-
this.
|
|
10740
|
+
await this.engine(["volume", "inspect", this.s.volume]);
|
|
10514
10741
|
} catch {
|
|
10742
|
+
return [];
|
|
10743
|
+
}
|
|
10744
|
+
const script = `cd ${VOLUME_ROOT}/mirrors 2>/dev/null || exit 0; find . -name '*.git' -type d -prune | while read -r d; do printf '%s\\t%s\\n' "\${d#./}" "$(cat "$d/crew-last-refresh" 2>/dev/null)"; done`;
|
|
10745
|
+
const { stdout } = await this.helper(this.s.helperImage, script, []);
|
|
10746
|
+
const rows = [];
|
|
10747
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
10748
|
+
const [rel, when] = line.split(" ");
|
|
10749
|
+
const m = /^([^/]+)\/(.+)\.git$/.exec(rel ?? "");
|
|
10750
|
+
if (m) rows.push({ repo: { host: m[1], path: m[2] }, lastRefresh: when?.trim() || null });
|
|
10515
10751
|
}
|
|
10752
|
+
return rows;
|
|
10516
10753
|
}
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
return;
|
|
10754
|
+
/** Drops mirrors (and their dependency trees) for repositories not in `keep`. Returns what was dropped. */
|
|
10755
|
+
async prune(keep) {
|
|
10756
|
+
const have = new Set(keep.map((r) => `${r.host}/${r.path}`));
|
|
10757
|
+
const drop = (await this.list()).map((m) => m.repo).filter((r) => !have.has(`${r.host}/${r.path}`));
|
|
10758
|
+
for (const r of drop) {
|
|
10759
|
+
await this.helper(this.s.helperImage, `rm -rf ${VOLUME_ROOT}/${q(mirrorRelPath(r))} ${VOLUME_ROOT}/${q(depsRelDir(r))}`, []);
|
|
10524
10760
|
}
|
|
10761
|
+
return drop;
|
|
10762
|
+
}
|
|
10763
|
+
/** Recreates the volume empty; the next pass refreshes every mirror. */
|
|
10764
|
+
async reset() {
|
|
10525
10765
|
try {
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
if (got.numTurns !== void 0 || got.costUsd !== void 0) this.sawResult = true;
|
|
10530
|
-
}
|
|
10531
|
-
this.handoff.feed(parsed);
|
|
10532
|
-
this.consume(parsed, this.mapper.map(parsed));
|
|
10533
|
-
} catch {
|
|
10766
|
+
await this.engine(["volume", "rm", "--force", this.s.volume]);
|
|
10767
|
+
} catch (e) {
|
|
10768
|
+
throw new Error(`could not remove volume ${this.s.volume}: ${describe(e)}`);
|
|
10534
10769
|
}
|
|
10770
|
+
await this.ensureVolume();
|
|
10535
10771
|
}
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
level: "info",
|
|
10572
|
-
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
10573
|
-
role: plan.role,
|
|
10574
|
-
...this.liveTicket ? { ticket: this.liveTicket } : {},
|
|
10575
|
-
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
10576
|
-
};
|
|
10577
|
-
if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
|
|
10578
|
-
`);
|
|
10579
|
-
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
10580
|
-
turnText += `${ev.text}
|
|
10581
|
-
`;
|
|
10582
|
-
const cycleIndex = this.cycles.length;
|
|
10583
|
-
this.cycles.push({
|
|
10584
|
-
cycleIndex,
|
|
10585
|
-
occurredAt: e.at,
|
|
10586
|
-
thinking: ev.text,
|
|
10587
|
-
// A conversational `text` turn is tagged 'assistant' so it's
|
|
10588
|
-
// distinguishable from a thinking block once both share this
|
|
10589
|
-
// buffer (ISSUE-594) — matching the precedent
|
|
10590
|
-
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
10591
|
-
// for the same column. A `thought` cycle stays untagged, same
|
|
10592
|
-
// as before this change.
|
|
10593
|
-
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
10594
|
-
});
|
|
10595
|
-
if (!this.muted) {
|
|
10596
|
-
this.eventsSink?.write(`${JSON.stringify({
|
|
10597
|
-
...e,
|
|
10598
|
-
message: "cycle",
|
|
10599
|
-
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
10600
|
-
})}
|
|
10601
|
-
`);
|
|
10602
|
-
}
|
|
10772
|
+
};
|
|
10773
|
+
function describe(e) {
|
|
10774
|
+
const err3 = e;
|
|
10775
|
+
return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
|
|
10776
|
+
}
|
|
10777
|
+
function cacheSettingsOf(cfg) {
|
|
10778
|
+
if (!cfg?.cacheVolume) return null;
|
|
10779
|
+
return {
|
|
10780
|
+
engine: cfg.engine ?? DEFAULT_CACHE_SETTINGS.engine,
|
|
10781
|
+
volume: cfg.cacheVolume,
|
|
10782
|
+
workspace: cfg.cacheWorkspace ?? DEFAULT_CACHE_SETTINGS.workspace,
|
|
10783
|
+
helperImage: cfg.cacheImage ?? DEFAULT_CACHE_SETTINGS.helperImage,
|
|
10784
|
+
depsImage: cfg.depsImage ?? cfg.image,
|
|
10785
|
+
depsCommand: cfg.depsCommand,
|
|
10786
|
+
depsLockfile: cfg.depsLockfile ?? DEFAULT_CACHE_SETTINGS.depsLockfile
|
|
10787
|
+
};
|
|
10788
|
+
}
|
|
10789
|
+
var sandboxCacheKey = (volume, r) => `sandbox_cache:${volume}:${r.host}/${r.path}`;
|
|
10790
|
+
var CACHE_REFRESH_INTERVAL_MS = 6e4;
|
|
10791
|
+
function makeCachePass(o) {
|
|
10792
|
+
const now = o.now ?? Date.now;
|
|
10793
|
+
let last = -Infinity;
|
|
10794
|
+
return async () => {
|
|
10795
|
+
if (now() - last < CACHE_REFRESH_INTERVAL_MS) return [];
|
|
10796
|
+
last = now();
|
|
10797
|
+
const all = [];
|
|
10798
|
+
for (const r of o.repos()) {
|
|
10799
|
+
let release;
|
|
10800
|
+
try {
|
|
10801
|
+
release = o.lock(r.dir);
|
|
10802
|
+
all.push(...await o.cache.refresh([r]));
|
|
10803
|
+
} catch (e) {
|
|
10804
|
+
all.push({ repo: r, ok: false, detail: describe(e) });
|
|
10805
|
+
} finally {
|
|
10806
|
+
release?.();
|
|
10603
10807
|
}
|
|
10604
10808
|
}
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10809
|
+
for (const out of all) {
|
|
10810
|
+
const key = sandboxCacheKey(o.volume, out.repo);
|
|
10811
|
+
if (out.ok) o.ok(key);
|
|
10812
|
+
else await o.fail(key, `sandbox cache: ${out.repo.host}/${out.repo.path} did not refresh (${out.detail}); runs use the previous mirror. Resumes automatically when a later pass refreshes it`);
|
|
10813
|
+
}
|
|
10814
|
+
return all;
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10817
|
+
|
|
10818
|
+
// src/runner.ts
|
|
10819
|
+
var execFileP2 = promisify2(execFile2);
|
|
10820
|
+
var DEFAULT_SANDBOX_CONFIG = {
|
|
10821
|
+
bin: "openshell",
|
|
10822
|
+
mirrorDir: "/cache",
|
|
10823
|
+
workDir: "/sandbox/work"
|
|
10609
10824
|
};
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
exitCode: null,
|
|
10623
|
-
running: true,
|
|
10624
|
-
...extra
|
|
10625
|
-
}));
|
|
10626
|
-
} catch {
|
|
10825
|
+
var SANDBOX_STDIN_LIMIT = 4 * 1024 * 1024 - 64 * 1024;
|
|
10826
|
+
var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
|
|
10827
|
+
var HostRunner = class {
|
|
10828
|
+
kind = "host";
|
|
10829
|
+
start(plan, o) {
|
|
10830
|
+
const child = spawn3(plan.bin, plan.args, {
|
|
10831
|
+
cwd: plan.cwd,
|
|
10832
|
+
env: o.env,
|
|
10833
|
+
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10834
|
+
detached: true
|
|
10835
|
+
});
|
|
10836
|
+
return { child, handle: { runner: "host", pid: child.pid } };
|
|
10627
10837
|
}
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
let moved = false;
|
|
10631
|
-
if (hintMoved) {
|
|
10632
|
-
try {
|
|
10633
|
-
moved = await hintMoved();
|
|
10634
|
-
} catch {
|
|
10635
|
-
}
|
|
10838
|
+
async kill(handle) {
|
|
10839
|
+
if (handle.pid !== void 0) await killRunGroup(handle.pid);
|
|
10636
10840
|
}
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
const { result } = obs;
|
|
10640
|
-
const pending = end.interrupted ? null : obs.handoff.pending();
|
|
10641
|
-
if (pending) {
|
|
10841
|
+
isAlive(handle) {
|
|
10842
|
+
if (handle.pid === void 0) return false;
|
|
10642
10843
|
try {
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
...pending,
|
|
10646
|
-
version: 1,
|
|
10647
|
-
route: id.route,
|
|
10648
|
-
role: id.role,
|
|
10649
|
-
...attributedTicket ? { ticket: attributedTicket } : {},
|
|
10650
|
-
streamPath: id.streamPath,
|
|
10651
|
-
createdAt: now.toISOString(),
|
|
10652
|
-
attempts: 0,
|
|
10653
|
-
nextAttemptAt: now.toISOString()
|
|
10654
|
-
});
|
|
10844
|
+
process.kill(handle.pid, 0);
|
|
10845
|
+
return true;
|
|
10655
10846
|
} catch (e) {
|
|
10656
|
-
|
|
10847
|
+
return e.code === "EPERM";
|
|
10657
10848
|
}
|
|
10658
10849
|
}
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
attributedTicket: attributedTicket ?? null,
|
|
10662
|
-
attributedBy,
|
|
10663
|
-
running: void 0,
|
|
10664
|
-
exitCode: end.code,
|
|
10665
|
-
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
10666
|
-
...pending ? { handoff: "pending" } : {},
|
|
10667
|
-
sessionId: result?.sessionId ?? null,
|
|
10668
|
-
numTurns: result?.numTurns ?? null,
|
|
10669
|
-
prefixTokens: obs.prefixTokens ?? null,
|
|
10670
|
-
totalCostUsd: result?.costUsd ?? null
|
|
10671
|
-
});
|
|
10672
|
-
} catch {
|
|
10850
|
+
reattach(meta) {
|
|
10851
|
+
return { runner: "host", pid: meta.pid };
|
|
10673
10852
|
}
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10853
|
+
async release() {
|
|
10854
|
+
}
|
|
10855
|
+
};
|
|
10856
|
+
function shQuote2(s) {
|
|
10857
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10858
|
+
}
|
|
10859
|
+
function sandboxName(plan, now = Date.now()) {
|
|
10860
|
+
const base = [plan.route, plan.role, plan.ticket ?? ""].join("-").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10861
|
+
return `crew-${base.slice(0, 36)}-${now.toString(36)}`.replace(/-+/g, "-");
|
|
10862
|
+
}
|
|
10863
|
+
var SandboxRunner = class {
|
|
10864
|
+
kind = "sandbox";
|
|
10865
|
+
cfg;
|
|
10866
|
+
/** Injected for tests; the real one reads the plan's checkout. */
|
|
10867
|
+
originOf;
|
|
10868
|
+
constructor(cfg = {}, originOf = gitOrigin) {
|
|
10869
|
+
this.cfg = { ...DEFAULT_SANDBOX_CONFIG, ...cfg };
|
|
10870
|
+
this.originOf = originOf;
|
|
10871
|
+
}
|
|
10872
|
+
os(args, o = {}) {
|
|
10873
|
+
return execFileP2(this.cfg.bin, args, { env: o.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
|
|
10874
|
+
}
|
|
10875
|
+
async start(plan, o) {
|
|
10876
|
+
const origin = this.originOf(plan.cwd);
|
|
10877
|
+
if (!origin) throw new Error(`cannot find the origin remote of ${plan.cwd}, so there is nothing to clone into the sandbox`);
|
|
10878
|
+
const repo = parseRemote(origin);
|
|
10879
|
+
if (!repo) throw new Error(`cannot read a forge host and path from the origin remote ${origin}`);
|
|
10880
|
+
const name = sandboxName(plan);
|
|
10881
|
+
const { cfg } = this;
|
|
10882
|
+
const secretKeys = Object.keys(plan.setEnv);
|
|
10883
|
+
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0 && !secretKeys.includes(cfg.forgeTokenVar)) secretKeys.push(cfg.forgeTokenVar);
|
|
10884
|
+
const createEnv = { ...process.env, ...plan.setEnv };
|
|
10885
|
+
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0) createEnv[cfg.forgeTokenVar] = o.env[cfg.forgeTokenVar];
|
|
10886
|
+
const create2 = ["sandbox", "create", "--name", name, "--detach"];
|
|
10887
|
+
if (cfg.policy) create2.push("--policy", cfg.policy);
|
|
10888
|
+
if (cfg.image) create2.push("--from", cfg.image);
|
|
10889
|
+
if (cfg.cacheVolume) {
|
|
10890
|
+
create2.push("--driver-config-json", JSON.stringify({
|
|
10891
|
+
docker: { mounts: [{ type: "volume", source: cfg.cacheVolume, target: cfg.mirrorDir, read_only: true }] }
|
|
10892
|
+
}));
|
|
10687
10893
|
}
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
outcome: end.interrupted ? "interrupted" : end.code === 0 || end.code === null && obs.sawResult ? "success" : "error",
|
|
10693
|
-
startedAt,
|
|
10694
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10695
|
-
cycles: obs.cycles,
|
|
10696
|
-
model: id.model,
|
|
10697
|
-
tokensIn: result?.inputTokens,
|
|
10698
|
-
tokensOut: result?.outputTokens,
|
|
10699
|
-
cacheReadTokens: result?.cacheReadTokens,
|
|
10700
|
-
cacheWriteTokens: result?.cacheWriteTokens,
|
|
10701
|
-
costUsd: result?.costUsd,
|
|
10702
|
-
// Both come from a finished run: without the result line (interrupted) neither is trustworthy.
|
|
10703
|
-
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
10704
|
-
...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
|
|
10705
|
-
harness: identity.harness,
|
|
10706
|
-
provider: identity.provider,
|
|
10707
|
-
promptVersion: id.promptVersion,
|
|
10708
|
-
promptSha: id.promptSha
|
|
10709
|
-
};
|
|
10894
|
+
for (const k of secretKeys) create2.push("--env-from", k);
|
|
10895
|
+
const maxSeconds = Math.max(60, Math.ceil((plan.maxRunMinutes ?? 0) * 60) + 600);
|
|
10896
|
+
create2.push("--", "sleep", String(plan.maxRunMinutes ? maxSeconds : 86400));
|
|
10897
|
+
const t0 = Date.now();
|
|
10710
10898
|
try {
|
|
10711
|
-
|
|
10712
|
-
try {
|
|
10713
|
-
clearShipAttention(new State(stateDirOfStream(id.streamPath)), agentLogRefusedKey(id.route));
|
|
10714
|
-
} catch {
|
|
10715
|
-
}
|
|
10716
|
-
if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
|
|
10717
|
-
if (sent.counterRetry) {
|
|
10718
|
-
try {
|
|
10719
|
-
queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
10720
|
-
version: 1,
|
|
10721
|
-
route: id.route,
|
|
10722
|
-
role: id.role,
|
|
10723
|
-
target: id.agentLog,
|
|
10724
|
-
write: sent.counterRetry,
|
|
10725
|
-
attempts: 0,
|
|
10726
|
-
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10727
|
-
});
|
|
10728
|
-
} catch {
|
|
10729
|
-
}
|
|
10730
|
-
}
|
|
10899
|
+
await this.os(create2, { env: createEnv });
|
|
10731
10900
|
} catch (e) {
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10901
|
+
await this.deleteQuietly(name);
|
|
10902
|
+
throw new Error(`openshell could not create the sandbox: ${describeExecError(e)}`);
|
|
10903
|
+
}
|
|
10904
|
+
o.emit.emit(`sandbox_created: ${name}`, { role: plan.role, data: { event: "sandbox_created", sandbox: name, ms: Date.now() - t0 } });
|
|
10905
|
+
const handle = { runner: "sandbox", sandbox: name, sandboxBin: cfg.bin };
|
|
10906
|
+
try {
|
|
10907
|
+
const branch = `crew-run-${name.slice(-8)}`;
|
|
10908
|
+
const clone = cloneScript({
|
|
10909
|
+
mirrorDir: cfg.mirrorDir,
|
|
10910
|
+
repo,
|
|
10911
|
+
workDir: cfg.workDir,
|
|
10912
|
+
origin,
|
|
10913
|
+
branch,
|
|
10914
|
+
lockfile: cfg.depsLockfile ?? DEFAULT_CACHE_SETTINGS.depsLockfile
|
|
10915
|
+
});
|
|
10916
|
+
await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
|
|
10917
|
+
const harness = cfg.agentBin ?? basename7(plan.bin);
|
|
10918
|
+
const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
|
|
10919
|
+
if (viaUpload) {
|
|
10920
|
+
const tmp = mkdtempSync(join25(tmpdir3(), "crew-prompt-"));
|
|
10749
10921
|
try {
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10922
|
+
const file = join25(tmp, "prompt.txt");
|
|
10923
|
+
writeFileSync14(file, plan.prompt, { mode: 384 });
|
|
10924
|
+
await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
|
|
10925
|
+
} finally {
|
|
10926
|
+
rmSync13(tmp, { recursive: true, force: true });
|
|
10755
10927
|
}
|
|
10756
10928
|
}
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
const eventsSink = openSink(plan.eventsPath);
|
|
10769
|
-
const id = identityOf(plan, adapter.harness);
|
|
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}`));
|
|
10929
|
+
const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
|
|
10930
|
+
const child = spawn3(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
|
|
10931
|
+
env: process.env,
|
|
10932
|
+
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10933
|
+
detached: true
|
|
10934
|
+
});
|
|
10935
|
+
return { child, handle: { ...handle, pid: child.pid }, ...viaUpload ? { promptDelivered: true } : {} };
|
|
10936
|
+
} catch (e) {
|
|
10937
|
+
await this.release(handle, o.emit);
|
|
10938
|
+
throw new Error(`could not prepare the sandbox ${name}: ${describeExecError(e)}`);
|
|
10939
|
+
}
|
|
10777
10940
|
}
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
};
|
|
10782
|
-
let begun;
|
|
10783
|
-
try {
|
|
10784
|
-
begun = runner.start(plan, { env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv }, streamFd, emit });
|
|
10785
|
-
} catch (e) {
|
|
10786
|
-
closeSync3(streamFd);
|
|
10787
|
-
return failed(e);
|
|
10941
|
+
async kill(handle) {
|
|
10942
|
+
if (handle.sandbox) await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
|
|
10943
|
+
if (handle.pid !== void 0) await killRunGroup(handle.pid, { graceMs: 2e3 });
|
|
10788
10944
|
}
|
|
10789
|
-
|
|
10790
|
-
return
|
|
10791
|
-
(run3) => {
|
|
10792
|
-
closeSync3(streamFd);
|
|
10793
|
-
return supervise(run3);
|
|
10794
|
-
},
|
|
10795
|
-
(e) => {
|
|
10796
|
-
closeSync3(streamFd);
|
|
10797
|
-
return failed(e);
|
|
10798
|
-
}
|
|
10799
|
-
);
|
|
10945
|
+
isAlive(handle) {
|
|
10946
|
+
return new HostRunner().isAlive(handle);
|
|
10800
10947
|
}
|
|
10801
|
-
|
|
10802
|
-
|
|
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
|
-
}
|
|
10833
|
-
}
|
|
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);
|
|
10881
|
-
});
|
|
10948
|
+
reattach(meta) {
|
|
10949
|
+
return { runner: "sandbox", pid: meta.pid, sandbox: meta.sandbox, sandboxBin: meta.sandboxBin };
|
|
10882
10950
|
}
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
obs.muted = false;
|
|
10894
|
-
const runner = runnerOfSidecar(sc);
|
|
10895
|
-
const adopted = isAlive(sc);
|
|
10896
|
-
let killed;
|
|
10897
|
-
if (adopted) {
|
|
10898
|
-
o.onAdopted?.();
|
|
10899
|
-
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
10900
|
-
while (isAlive(sc)) {
|
|
10901
|
-
obs.feed(tail3.poll());
|
|
10902
|
-
if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
|
|
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);
|
|
10904
|
-
continue;
|
|
10905
|
-
}
|
|
10906
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
10951
|
+
async release(handle, emit) {
|
|
10952
|
+
if (!handle.sandbox) return;
|
|
10953
|
+
const t0 = Date.now();
|
|
10954
|
+
await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
|
|
10955
|
+
emit.emit(`sandbox_deleted: ${handle.sandbox}`, { data: { event: "sandbox_deleted", sandbox: handle.sandbox, ms: Date.now() - t0 } });
|
|
10956
|
+
}
|
|
10957
|
+
async deleteQuietly(name, bin = this.cfg.bin) {
|
|
10958
|
+
try {
|
|
10959
|
+
await execFileP2(bin, ["sandbox", "delete", name], { timeout: 6e4 });
|
|
10960
|
+
} catch {
|
|
10907
10961
|
}
|
|
10908
10962
|
}
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
10914
|
-
const { ms: ms3, ticket } = await finishRun(
|
|
10915
|
-
id,
|
|
10916
|
-
obs,
|
|
10917
|
-
emit,
|
|
10918
|
-
sc.spawnedAt,
|
|
10919
|
-
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
10920
|
-
o.agentLogKey
|
|
10921
|
-
);
|
|
10922
|
-
return { adopted, ...interrupted ? { interrupted } : {}, ...killed ? { killed } : {}, ms: ms3, ticket };
|
|
10923
|
-
}
|
|
10924
|
-
|
|
10925
|
-
// src/seat-keys.ts
|
|
10926
|
-
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
10927
|
-
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
10928
|
-
function keyIsAdmin(route2) {
|
|
10929
|
-
const role = route2.resolved?.role;
|
|
10930
|
-
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
10963
|
+
};
|
|
10964
|
+
function describeExecError(e) {
|
|
10965
|
+
const err3 = e;
|
|
10966
|
+
return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
|
|
10931
10967
|
}
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10968
|
+
function gitOrigin(cwd) {
|
|
10969
|
+
try {
|
|
10970
|
+
return execFileSync7("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
10971
|
+
} catch {
|
|
10972
|
+
return null;
|
|
10973
|
+
}
|
|
10937
10974
|
}
|
|
10938
|
-
function
|
|
10939
|
-
|
|
10940
|
-
return seatKeySlug(workspaceSlug, agentId);
|
|
10975
|
+
function runnerFor(kind, sandbox) {
|
|
10976
|
+
return kind === "sandbox" ? new SandboxRunner(sandbox) : new HostRunner();
|
|
10941
10977
|
}
|
|
10942
|
-
function
|
|
10943
|
-
return
|
|
10978
|
+
function runnerOfSidecar(sc) {
|
|
10979
|
+
return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
|
|
10944
10980
|
}
|
|
10945
|
-
|
|
10946
|
-
const
|
|
10947
|
-
|
|
10948
|
-
if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
10981
|
+
function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync7(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
|
|
10982
|
+
const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
|
|
10983
|
+
let version;
|
|
10949
10984
|
try {
|
|
10950
|
-
|
|
10951
|
-
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10952
|
-
return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10985
|
+
version = run3(bin, ["--version"]).trim();
|
|
10953
10986
|
} catch (e) {
|
|
10954
|
-
|
|
10955
|
-
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
10987
|
+
return { ok: false, detail: `${bin} is not installed or not on the scheduler PATH (${e.message.split("\n")[0]})` };
|
|
10956
10988
|
}
|
|
10957
|
-
}
|
|
10958
|
-
async function resolveSeatKey(route2, role, getStore = getSessionStore) {
|
|
10959
|
-
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
10960
|
-
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
10961
|
-
const agentId = seatAgentId(route2, role);
|
|
10962
|
-
if (!agentId) return ship(keyIsAdmin(route2) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
10963
10989
|
try {
|
|
10964
|
-
|
|
10965
|
-
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10966
|
-
return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10990
|
+
run3(bin, ["status"]);
|
|
10967
10991
|
} catch (e) {
|
|
10968
|
-
const
|
|
10969
|
-
return {
|
|
10970
|
-
|
|
10971
|
-
}
|
|
10972
|
-
|
|
10973
|
-
const err3 = e;
|
|
10974
|
-
const raw = typeof err3?.stderr === "string" && err3.stderr.trim() ? err3.stderr : typeof err3?.message === "string" ? err3.message : "";
|
|
10975
|
-
for (const line of raw.split("\n")) {
|
|
10976
|
-
const t = line.trim();
|
|
10977
|
-
if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
|
|
10978
|
-
return t.slice(0, 120);
|
|
10992
|
+
const err3 = e;
|
|
10993
|
+
return {
|
|
10994
|
+
ok: false,
|
|
10995
|
+
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.`
|
|
10996
|
+
};
|
|
10979
10997
|
}
|
|
10980
|
-
return
|
|
10998
|
+
return { ok: true, detail: version };
|
|
10981
10999
|
}
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
const
|
|
10989
|
-
|
|
11000
|
+
|
|
11001
|
+
// src/agent.ts
|
|
11002
|
+
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
11003
|
+
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
11004
|
+
var LEGACY_TOOLCHAIN_PROVIDER = "anthropic";
|
|
11005
|
+
function scrubbedEnv(env = process.env, harness = LEGACY_TOOLCHAIN_HARNESS) {
|
|
11006
|
+
const out = { ...env };
|
|
11007
|
+
for (const v of adapterFor(harness).unsetEnv) delete out[v];
|
|
11008
|
+
return out;
|
|
10990
11009
|
}
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
11010
|
+
var CREW_LANE_ROLE_VAR = "CREW_LANE_ROLE";
|
|
11011
|
+
function assemblePrompt(p) {
|
|
11012
|
+
const sections = [p.roster, p.environment, `${p.common}${p.brief}`];
|
|
11013
|
+
if (p.digest) sections.push(p.digest);
|
|
11014
|
+
return sections.join("\n\n");
|
|
10994
11015
|
}
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
11016
|
+
var AgentError = class extends Error {
|
|
11017
|
+
};
|
|
11018
|
+
var AgentSpawnError = class extends AgentError {
|
|
11019
|
+
};
|
|
11020
|
+
var WorkspaceAuthRefused = class extends AgentSpawnError {
|
|
11021
|
+
};
|
|
11022
|
+
var WorkspaceAllowanceExhausted = class extends AgentError {
|
|
11023
|
+
};
|
|
11024
|
+
var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
|
|
11025
|
+
function streamShowsAllowanceRefusal(streamPath) {
|
|
11026
|
+
let raw;
|
|
11027
|
+
try {
|
|
11028
|
+
raw = readFileSync21(streamPath, "utf8");
|
|
11029
|
+
} catch {
|
|
11030
|
+
return false;
|
|
11031
|
+
}
|
|
11032
|
+
if (!raw.includes(ALLOWANCE_REFUSAL_MARKER)) return false;
|
|
11033
|
+
for (const line of raw.split("\n")) {
|
|
11034
|
+
if (!line.includes(ALLOWANCE_REFUSAL_MARKER)) continue;
|
|
11035
|
+
let ev;
|
|
11036
|
+
try {
|
|
11037
|
+
ev = JSON.parse(line);
|
|
11038
|
+
} catch {
|
|
11039
|
+
continue;
|
|
11040
|
+
}
|
|
11041
|
+
if (typeof ev !== "object" || ev === null) continue;
|
|
11042
|
+
const e = ev;
|
|
11043
|
+
if (e.type === "result" && e.is_error === true) {
|
|
11044
|
+
if (typeof e.result === "string" && e.result.includes(ALLOWANCE_REFUSAL_MARKER)) return true;
|
|
11045
|
+
} else if (e.type === "assistant" && (e.isApiErrorMessage === true || e.error !== void 0)) {
|
|
11046
|
+
if (JSON.stringify(e.message ?? "").includes(ALLOWANCE_REFUSAL_MARKER)) return true;
|
|
11047
|
+
}
|
|
11048
|
+
}
|
|
11049
|
+
return false;
|
|
11050
|
+
}
|
|
11051
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join26(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11052
|
+
function planAgentRun(o) {
|
|
11053
|
+
const briefPath = join26(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
11054
|
+
if (!existsSync24(briefPath)) {
|
|
11055
|
+
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
11056
|
+
}
|
|
11057
|
+
const commonPath = join26(o.route.promptsDir, "personas", "common.md");
|
|
11058
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync24(commonPath);
|
|
11059
|
+
const now = o.now ?? (() => Date.now());
|
|
11060
|
+
const routeLabel = routeSlug(o.route.route);
|
|
11061
|
+
const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
|
|
11062
|
+
const digestPath2 = ownDigest && existsSync24(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
|
|
11063
|
+
let digest;
|
|
11064
|
+
let ageSeconds;
|
|
11065
|
+
if (existsSync24(digestPath2)) {
|
|
11066
|
+
ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
|
|
11067
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync21(digestPath2, "utf8");
|
|
11068
|
+
}
|
|
11069
|
+
const common = usesCommon ? readFileSync21(commonPath, "utf8") : "";
|
|
11070
|
+
const brief = readFileSync21(briefPath, "utf8");
|
|
11071
|
+
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
11072
|
+
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
11073
|
+
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
11074
|
+
let expanded;
|
|
11075
|
+
try {
|
|
11076
|
+
expanded = expandSkillMarkers(
|
|
11077
|
+
`${effectiveCommon}${effectiveBrief}`,
|
|
11078
|
+
(name) => o.skillOverrides?.[name] ?? localSkills.get(name)
|
|
11079
|
+
);
|
|
11080
|
+
} catch (e) {
|
|
11081
|
+
if (e instanceof SkillIncludeError) throw new AgentError(`${e.message} \u2014 refusing to run an unscoped session`);
|
|
11082
|
+
throw e;
|
|
11083
|
+
}
|
|
11084
|
+
const prompt = assemblePrompt({
|
|
11085
|
+
roster: o.roster,
|
|
11086
|
+
environment: o.environment,
|
|
11087
|
+
common: "",
|
|
11088
|
+
brief: expanded.text,
|
|
11089
|
+
digest
|
|
11090
|
+
});
|
|
11091
|
+
const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
|
|
11092
|
+
const streamsDir = join26(o.stateDir, "streams");
|
|
11093
|
+
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
11094
|
+
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
11095
|
+
const { model } = seat;
|
|
11096
|
+
let toolchain = seat.toolchain;
|
|
11097
|
+
if (toolchain.auth === "workspace") {
|
|
11098
|
+
if (!o.apiKey || o.seatKind === "ship") {
|
|
11099
|
+
throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, which needs the seat's own agent key \u2014 this seat would fall back to the ship key (run \`crew agents sync\`)`);
|
|
11100
|
+
}
|
|
11101
|
+
if (!o.route.resolved) {
|
|
11102
|
+
throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, but route ${o.route.route} is not connected (run \`crew connect\`)`);
|
|
11103
|
+
}
|
|
11104
|
+
toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
|
|
11105
|
+
}
|
|
11106
|
+
const adapter = adapterFor(toolchain.harness);
|
|
11107
|
+
const invocation = adapter.invocation(toolchain, {
|
|
11108
|
+
role: o.role,
|
|
11109
|
+
model,
|
|
11110
|
+
// Skipped when there's no key to send: a route `crew connect` hasn't
|
|
11111
|
+
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
11112
|
+
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
11113
|
+
});
|
|
11114
|
+
const columns = resolveContract(o.route.contract).columns;
|
|
11115
|
+
const agentLog = o.route.resolved && o.apiKey ? {
|
|
11116
|
+
baseUrl: o.route.baseUrl,
|
|
11117
|
+
workspaceId: o.route.resolved.workspaceId,
|
|
11118
|
+
apiKey: o.apiKey,
|
|
11119
|
+
userAgent: o.ship.userAgent,
|
|
11120
|
+
agentId: o.resolvedAgentId,
|
|
11121
|
+
...o.route.resolved.models?.issues ? { tickets: {
|
|
11122
|
+
issuesModelId: o.route.resolved.models.issues,
|
|
11123
|
+
keyColumn: columns.key,
|
|
11124
|
+
tagColumn: columns.tag,
|
|
11125
|
+
counters: {
|
|
11126
|
+
agentRuns: columns.agentRuns,
|
|
11127
|
+
tokensIn: columns.tokensIn,
|
|
11128
|
+
tokensOut: columns.tokensOut,
|
|
11129
|
+
cacheReadTokens: columns.cacheReadTokens,
|
|
11130
|
+
cacheWriteTokens: columns.cacheWriteTokens,
|
|
11131
|
+
costUsd: columns.costUsd
|
|
11132
|
+
}
|
|
11133
|
+
} } : {}
|
|
11134
|
+
} : void 0;
|
|
11135
|
+
return {
|
|
11136
|
+
role: o.role,
|
|
11137
|
+
route: routeLabel,
|
|
11138
|
+
// The route's directory, which is a STARTING POINT and not the
|
|
11139
|
+
// ticket's repo: the session picks its own working directory even when
|
|
11140
|
+
// the runner already selected a ticket (`o.repo` is known then, and
|
|
11141
|
+
// only feeds the per-repo toolchain choice). Where an area spans
|
|
11142
|
+
// several repos this is whichever one `repos` happened to list first
|
|
11143
|
+
// (config.ts), and the digest's `repo` column is what actually places
|
|
11144
|
+
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
11145
|
+
cwd: o.route.dir,
|
|
11146
|
+
bin: invocation.bin,
|
|
11147
|
+
args: invocation.args,
|
|
11148
|
+
toolchain: toolchain.name,
|
|
11149
|
+
harness: toolchain.harness,
|
|
11150
|
+
workspaceAuth: toolchain.auth === "workspace",
|
|
11151
|
+
provider: toolchainProvider(toolchain),
|
|
11152
|
+
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
11153
|
+
...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
|
|
11154
|
+
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
11155
|
+
warnings: [...seat.warnings ?? [], ...expanded.warnings],
|
|
11156
|
+
prompt,
|
|
11157
|
+
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
11158
|
+
unsetEnv: adapter.unsetEnv,
|
|
11159
|
+
setEnv: {
|
|
11160
|
+
...o.apiKey ? { [API_KEY_VAR]: o.apiKey } : {},
|
|
11161
|
+
...adapter.runEnv(toolchain),
|
|
11162
|
+
// The operator's global SessionStart hook (pair-context-hook.sh) injects
|
|
11163
|
+
// the interactive Pair persona's prompt for any session it can't tell
|
|
11164
|
+
// apart from a person's own terminal. Every headless lane run this
|
|
11165
|
+
// planner produces carries a real ticket brief already (`prompt`
|
|
11166
|
+
// above) — it must never be replaced by the Pair persona with no
|
|
11167
|
+
// ticket attached, which silently no-ops and strands the ticket
|
|
11168
|
+
// in_progress (ISSUE-490, ISSUE-591). Setting this is what lets that
|
|
11169
|
+
// hook tell the two apart; it has nothing to do with the run itself.
|
|
11170
|
+
[CREW_LANE_ROLE_VAR]: o.role,
|
|
11171
|
+
// CREW-2783: the ship's forge credential — git helper config plus the
|
|
11172
|
+
// token as an env var for `gh` and friends. Empty without `forges:`.
|
|
11173
|
+
...forgeRunEnv(o.ship.forges, { crewCommand: crewCommandLine(), configFile: process.env.CREW_CONFIG, stateDir: o.stateDir })
|
|
11174
|
+
},
|
|
11175
|
+
digestAttached: digest !== void 0,
|
|
11176
|
+
digestAgeSeconds: ageSeconds,
|
|
11177
|
+
model,
|
|
11178
|
+
streamPath: join26(streamsDir, `${base}.jsonl`),
|
|
11179
|
+
eventsPath: join26(streamsDir, `${base}.events.jsonl`),
|
|
11180
|
+
ticket: o.ticket,
|
|
11181
|
+
ticketRecordId: o.ticketRecordId ?? null,
|
|
11182
|
+
repo: o.repo ?? null,
|
|
11183
|
+
routeName: o.route.route,
|
|
11184
|
+
agentLog,
|
|
11185
|
+
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
11186
|
+
promptSha
|
|
11187
|
+
};
|
|
11188
|
+
}
|
|
11189
|
+
function describePlan(p) {
|
|
11190
|
+
return [
|
|
11191
|
+
`role: ${p.role}`,
|
|
11192
|
+
...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}${p.provider ? `; provider ${p.provider}` : ""}`] : [],
|
|
11193
|
+
...(p.warnings ?? []).map((w) => `warning: ${w}`),
|
|
11194
|
+
// Only for a non-default toolchain, so a plain Claude run's output is unchanged.
|
|
11195
|
+
...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
|
|
11196
|
+
`cwd: ${p.cwd}`,
|
|
11197
|
+
`command: ${p.bin} ${p.args.join(" ")}`,
|
|
11198
|
+
`prompt: ${p.promptBytes} bytes on stdin (never argv)`,
|
|
11199
|
+
`digest: ${p.digestAttached ? `attached, ${p.digestAgeSeconds}s old` : "absent \u2014 the agent will fetch the tracker itself"}`,
|
|
11200
|
+
`unset: ${p.unsetEnv.join(" ")}`,
|
|
11201
|
+
// The VALUE is deliberately absent: --dry-run output is pasted into
|
|
11202
|
+
// tickets and chat.
|
|
11203
|
+
`set: ${Object.keys(p.setEnv).join(" ") || "(nothing)"}`,
|
|
11204
|
+
`stream: ${p.streamPath}`
|
|
11205
|
+
].join("\n");
|
|
11206
|
+
}
|
|
11207
|
+
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
11208
|
+
function readRunSidecar(streamPath) {
|
|
11209
|
+
try {
|
|
11210
|
+
const sc = JSON.parse(readFileSync21(pidPathFor(streamPath), "utf8"));
|
|
11211
|
+
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
11212
|
+
} catch {
|
|
11213
|
+
return null;
|
|
11214
|
+
}
|
|
11215
|
+
}
|
|
11216
|
+
function runIsAlive(sc) {
|
|
11217
|
+
try {
|
|
11218
|
+
process.kill(sc.pid, 0);
|
|
11219
|
+
} catch (e) {
|
|
11220
|
+
if (e.code !== "EPERM") return false;
|
|
11221
|
+
}
|
|
11222
|
+
const ps = spawnSync6("ps", ["-o", "lstart=", "-p", String(sc.pid)], { encoding: "utf8" });
|
|
11223
|
+
const started = Date.parse(ps.stdout.trim());
|
|
11224
|
+
if (ps.status !== 0 || Number.isNaN(started)) return ps.status === 0;
|
|
11225
|
+
return Math.abs(started - Date.parse(sc.spawnedAt)) < 15e3;
|
|
11226
|
+
}
|
|
11227
|
+
function identityOf(plan, adapterHarness) {
|
|
11228
|
+
return {
|
|
11229
|
+
role: plan.role,
|
|
11230
|
+
route: plan.route,
|
|
11231
|
+
ticket: plan.ticket ?? null,
|
|
11232
|
+
model: plan.model,
|
|
11233
|
+
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
11234
|
+
harness: adapterHarness,
|
|
11235
|
+
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER,
|
|
11236
|
+
eventsPath: plan.eventsPath,
|
|
11237
|
+
streamPath: plan.streamPath,
|
|
11238
|
+
agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
|
|
11239
|
+
promptVersion: plan.promptVersion,
|
|
11240
|
+
promptSha: plan.promptSha
|
|
11241
|
+
};
|
|
11242
|
+
}
|
|
11243
|
+
async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
|
|
11244
|
+
const elapsedMs = Date.now() - Date.parse(spawnedAt);
|
|
11245
|
+
const limit = limitMinutes ?? Math.round(elapsedMs / 6e4);
|
|
11246
|
+
emit.emit(`run_killed: ${role} run exceeded its wall-clock limit of ${limit} min (ran ${Math.round(elapsedMs / 6e4)} min)`, {
|
|
11247
|
+
role,
|
|
11248
|
+
...ticket ? { ticket } : {},
|
|
11249
|
+
data: { event: "run_killed", reason: WALL_CLOCK_REASON, limitMinutes: limit, elapsedMs, pid }
|
|
11250
|
+
});
|
|
11251
|
+
await kill();
|
|
11252
|
+
return { limitMinutes: limit, elapsedMs };
|
|
11253
|
+
}
|
|
11254
|
+
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
11255
|
+
function openSink(path) {
|
|
11256
|
+
try {
|
|
11257
|
+
mkdirSync13(dirname9(path), { recursive: true });
|
|
11258
|
+
return createWriteStream(path, { flags: "a" });
|
|
11259
|
+
} catch {
|
|
11260
|
+
return void 0;
|
|
11261
|
+
}
|
|
11262
|
+
}
|
|
11263
|
+
function endSink(sink) {
|
|
11264
|
+
if (!sink) return Promise.resolve();
|
|
11265
|
+
sink.end();
|
|
11266
|
+
return finished(sink).catch(() => void 0);
|
|
11267
|
+
}
|
|
11268
|
+
var FileTail = class {
|
|
11269
|
+
offset = 0;
|
|
11270
|
+
decoder = new StringDecoder("utf8");
|
|
11271
|
+
path;
|
|
11272
|
+
constructor(path) {
|
|
11273
|
+
this.path = path;
|
|
11274
|
+
}
|
|
11275
|
+
poll() {
|
|
11276
|
+
let fd;
|
|
11277
|
+
try {
|
|
11278
|
+
fd = openSync3(this.path, "r");
|
|
11279
|
+
} catch {
|
|
11280
|
+
return "";
|
|
11281
|
+
}
|
|
11282
|
+
try {
|
|
11283
|
+
let text2 = "";
|
|
11284
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
11285
|
+
for (; ; ) {
|
|
11286
|
+
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
11287
|
+
if (n <= 0) break;
|
|
11288
|
+
this.offset += n;
|
|
11289
|
+
text2 += this.decoder.write(chunk.subarray(0, n));
|
|
11290
|
+
}
|
|
11291
|
+
return text2;
|
|
11292
|
+
} finally {
|
|
11293
|
+
closeSync3(fd);
|
|
11294
|
+
}
|
|
11295
|
+
}
|
|
11296
|
+
};
|
|
11297
|
+
var TAIL_POLL_MS = 200;
|
|
11298
|
+
var RunObserver = class {
|
|
11299
|
+
cycles = [];
|
|
11300
|
+
attribution = new TicketAttributionTracker();
|
|
11301
|
+
/** Hand-off writes and their outcomes, to tell a run whose hand-off never landed (CREW-2363). */
|
|
11302
|
+
handoff = new HandoffTranscriptTracker();
|
|
11303
|
+
result;
|
|
11304
|
+
/** Prefix tokens of the first assistant call that carried usage; kept once. */
|
|
11305
|
+
prefixTokens;
|
|
11306
|
+
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
11307
|
+
sawResult = false;
|
|
11308
|
+
muted = false;
|
|
11309
|
+
/**
|
|
11310
|
+
* The label stamped on events. Starts as the poll hint and switches to the
|
|
11311
|
+
* ticket the run actually claims (CREW-1776); `id.ticket` stays the hint.
|
|
11312
|
+
*/
|
|
11313
|
+
liveTicket;
|
|
11314
|
+
/** Called after `liveTicket` changes, so the sidecar can be rewritten. */
|
|
11315
|
+
onClaim;
|
|
11316
|
+
buf = "";
|
|
11317
|
+
mapper;
|
|
11318
|
+
id;
|
|
11319
|
+
adapter;
|
|
11320
|
+
eventsSink;
|
|
11321
|
+
emitCycle;
|
|
11322
|
+
constructor(id, adapter, eventsSink, emitCycle) {
|
|
11323
|
+
this.id = id;
|
|
11324
|
+
this.adapter = adapter;
|
|
11325
|
+
this.eventsSink = eventsSink;
|
|
11326
|
+
this.emitCycle = emitCycle;
|
|
11327
|
+
this.liveTicket = id.ticket ?? void 0;
|
|
11328
|
+
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
11329
|
+
}
|
|
11330
|
+
feed(text2) {
|
|
11331
|
+
this.buf += text2;
|
|
11332
|
+
const lines2 = this.buf.split("\n");
|
|
11333
|
+
this.buf = lines2.pop() ?? "";
|
|
11334
|
+
for (const line of lines2) this.handleLine(line);
|
|
11335
|
+
}
|
|
11336
|
+
end() {
|
|
11337
|
+
if (this.buf) this.handleLine(this.buf);
|
|
11338
|
+
this.buf = "";
|
|
11339
|
+
try {
|
|
11340
|
+
this.consume(void 0, this.mapper.flush());
|
|
11341
|
+
} catch {
|
|
11342
|
+
}
|
|
11343
|
+
}
|
|
11344
|
+
handleLine(line) {
|
|
11345
|
+
if (!line) return;
|
|
11346
|
+
let parsed;
|
|
11347
|
+
try {
|
|
11348
|
+
parsed = JSON.parse(line);
|
|
11349
|
+
} catch {
|
|
11350
|
+
return;
|
|
11351
|
+
}
|
|
11352
|
+
try {
|
|
11353
|
+
const got = this.adapter.extractResult(parsed);
|
|
11354
|
+
if (got) {
|
|
11355
|
+
this.result = { ...this.result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
11356
|
+
if (got.numTurns !== void 0 || got.costUsd !== void 0) this.sawResult = true;
|
|
11357
|
+
}
|
|
11358
|
+
this.handoff.feed(parsed);
|
|
11359
|
+
this.consume(parsed, this.mapper.map(parsed));
|
|
11360
|
+
} catch {
|
|
11361
|
+
}
|
|
10999
11362
|
}
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11363
|
+
// Turns mapped events (and, when there is a source line, its token usage)
|
|
11364
|
+
// into sink writes and buffered cycles. `parsed` is undefined for events
|
|
11365
|
+
// the mapper flushes at end of stream, which have no line of their own.
|
|
11366
|
+
consume(parsed, events) {
|
|
11367
|
+
const plan = this.id;
|
|
11368
|
+
let turnText = "";
|
|
11369
|
+
for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
|
|
11370
|
+
for (const ev of events) {
|
|
11371
|
+
if (ev.kind === "tool") this.attribution.noteWrite(ev.tool, ev.input);
|
|
11372
|
+
if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
|
|
11373
|
+
const claimed = this.attribution.current;
|
|
11374
|
+
if (claimed && claimed !== this.liveTicket) {
|
|
11375
|
+
if (!this.muted) {
|
|
11376
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
11377
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11378
|
+
cycle: this.emitCycle,
|
|
11379
|
+
route: plan.route,
|
|
11380
|
+
step: "agent",
|
|
11381
|
+
level: "info",
|
|
11382
|
+
message: `ticket: ${this.liveTicket ?? "none"} \u2192 ${claimed}`,
|
|
11383
|
+
role: plan.role,
|
|
11384
|
+
ticket: claimed,
|
|
11385
|
+
data: { kind: "ticket", from: this.liveTicket ?? null, to: claimed }
|
|
11386
|
+
})}
|
|
11387
|
+
`);
|
|
11388
|
+
}
|
|
11389
|
+
this.liveTicket = claimed;
|
|
11390
|
+
if (!this.muted) this.onClaim?.();
|
|
11391
|
+
}
|
|
11392
|
+
}
|
|
11393
|
+
const e = {
|
|
11394
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11395
|
+
cycle: this.emitCycle,
|
|
11396
|
+
route: plan.route,
|
|
11397
|
+
step: "agent",
|
|
11398
|
+
level: "info",
|
|
11399
|
+
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
11400
|
+
role: plan.role,
|
|
11401
|
+
...this.liveTicket ? { ticket: this.liveTicket } : {},
|
|
11402
|
+
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
11403
|
+
};
|
|
11404
|
+
if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
|
|
11405
|
+
`);
|
|
11406
|
+
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
11407
|
+
turnText += `${ev.text}
|
|
11408
|
+
`;
|
|
11409
|
+
const cycleIndex = this.cycles.length;
|
|
11410
|
+
this.cycles.push({
|
|
11411
|
+
cycleIndex,
|
|
11412
|
+
occurredAt: e.at,
|
|
11413
|
+
thinking: ev.text,
|
|
11414
|
+
// A conversational `text` turn is tagged 'assistant' so it's
|
|
11415
|
+
// distinguishable from a thinking block once both share this
|
|
11416
|
+
// buffer (ISSUE-594) — matching the precedent
|
|
11417
|
+
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
11418
|
+
// for the same column. A `thought` cycle stays untagged, same
|
|
11419
|
+
// as before this change.
|
|
11420
|
+
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
11421
|
+
});
|
|
11422
|
+
if (!this.muted) {
|
|
11423
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
11424
|
+
...e,
|
|
11425
|
+
message: "cycle",
|
|
11426
|
+
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
11427
|
+
})}
|
|
11428
|
+
`);
|
|
11429
|
+
}
|
|
11430
|
+
}
|
|
11431
|
+
}
|
|
11432
|
+
if (this.prefixTokens === void 0 && parsed !== void 0) this.prefixTokens = this.adapter.extractPrefixTokens?.(parsed);
|
|
11433
|
+
const turnTokens = parsed === void 0 ? void 0 : this.adapter.extractTurnTokens(parsed);
|
|
11434
|
+
if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
|
|
11435
|
+
}
|
|
11436
|
+
};
|
|
11437
|
+
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
11012
11438
|
try {
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11439
|
+
writeFileSync15(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
11440
|
+
route: id.route,
|
|
11441
|
+
role: id.role,
|
|
11442
|
+
hint: id.ticket,
|
|
11443
|
+
ticket: liveTicket ?? id.ticket,
|
|
11444
|
+
startedAt,
|
|
11445
|
+
model: id.model,
|
|
11446
|
+
toolchain: id.toolchain,
|
|
11447
|
+
harness: id.harness,
|
|
11448
|
+
provider: id.provider,
|
|
11449
|
+
exitCode: null,
|
|
11450
|
+
running: true,
|
|
11451
|
+
...extra
|
|
11019
11452
|
}));
|
|
11453
|
+
} catch {
|
|
11020
11454
|
}
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11455
|
+
}
|
|
11456
|
+
async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved) {
|
|
11457
|
+
let moved = false;
|
|
11458
|
+
if (hintMoved) {
|
|
11025
11459
|
try {
|
|
11026
|
-
await
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
|
|
11030
|
-
|
|
11460
|
+
moved = await hintMoved();
|
|
11461
|
+
} catch {
|
|
11462
|
+
}
|
|
11463
|
+
}
|
|
11464
|
+
const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket, { hintMoved: moved });
|
|
11465
|
+
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
11466
|
+
const { result } = obs;
|
|
11467
|
+
const pending = end.interrupted ? null : obs.handoff.pending();
|
|
11468
|
+
if (pending) {
|
|
11469
|
+
try {
|
|
11470
|
+
const now = /* @__PURE__ */ new Date();
|
|
11471
|
+
writePendingHandoff(stateDirOfStream(id.streamPath), {
|
|
11472
|
+
...pending,
|
|
11473
|
+
version: 1,
|
|
11474
|
+
route: id.route,
|
|
11475
|
+
role: id.role,
|
|
11476
|
+
...attributedTicket ? { ticket: attributedTicket } : {},
|
|
11477
|
+
streamPath: id.streamPath,
|
|
11478
|
+
createdAt: now.toISOString(),
|
|
11479
|
+
attempts: 0,
|
|
11480
|
+
nextAttemptAt: now.toISOString()
|
|
11031
11481
|
});
|
|
11032
|
-
await store.clear(host, probeSlug);
|
|
11033
11482
|
} catch (e) {
|
|
11034
|
-
|
|
11035
|
-
return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
|
|
11483
|
+
emit.warn(`could not save the pending hand-off: ${e.message}`, { step: "agent" });
|
|
11036
11484
|
}
|
|
11037
11485
|
}
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11486
|
+
try {
|
|
11487
|
+
writeRunMeta(id, startedAt, obs.liveTicket, {
|
|
11488
|
+
attributedTicket: attributedTicket ?? null,
|
|
11489
|
+
attributedBy,
|
|
11490
|
+
running: void 0,
|
|
11491
|
+
exitCode: end.code,
|
|
11492
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
11493
|
+
...pending ? { handoff: "pending" } : {},
|
|
11494
|
+
sessionId: result?.sessionId ?? null,
|
|
11495
|
+
numTurns: result?.numTurns ?? null,
|
|
11496
|
+
prefixTokens: obs.prefixTokens ?? null,
|
|
11497
|
+
totalCostUsd: result?.costUsd ?? null
|
|
11498
|
+
});
|
|
11499
|
+
} catch {
|
|
11500
|
+
}
|
|
11501
|
+
const ms3 = Date.now() - Date.parse(startedAt);
|
|
11502
|
+
emit.emit(end.interrupted ? "agent run interrupted" : `agent run finished (${pending ? "hand-off pending" : `exit ${end.code ?? "unknown"}`})`, {
|
|
11503
|
+
data: {
|
|
11504
|
+
ms: ms3,
|
|
11505
|
+
code: end.code,
|
|
11506
|
+
...pending ? { handoff: "pending" } : {},
|
|
11507
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
11508
|
+
...identity,
|
|
11509
|
+
attributedTicket: attributedTicket ?? null,
|
|
11510
|
+
attributedBy,
|
|
11511
|
+
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
11512
|
+
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
11513
|
+
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
11042
11514
|
}
|
|
11043
|
-
|
|
11515
|
+
});
|
|
11516
|
+
if (id.agentLog && agentLogKey) {
|
|
11517
|
+
const report = {
|
|
11518
|
+
ticketReference: attributedTicket,
|
|
11519
|
+
outcome: end.interrupted ? "interrupted" : end.code === 0 || end.code === null && obs.sawResult ? "success" : "error",
|
|
11520
|
+
startedAt,
|
|
11521
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11522
|
+
cycles: obs.cycles,
|
|
11523
|
+
model: id.model,
|
|
11524
|
+
tokensIn: result?.inputTokens,
|
|
11525
|
+
tokensOut: result?.outputTokens,
|
|
11526
|
+
cacheReadTokens: result?.cacheReadTokens,
|
|
11527
|
+
cacheWriteTokens: result?.cacheWriteTokens,
|
|
11528
|
+
costUsd: result?.costUsd,
|
|
11529
|
+
// Both come from a finished run: without the result line (interrupted) neither is trustworthy.
|
|
11530
|
+
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
11531
|
+
...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
|
|
11532
|
+
harness: identity.harness,
|
|
11533
|
+
provider: identity.provider,
|
|
11534
|
+
promptVersion: id.promptVersion,
|
|
11535
|
+
promptSha: id.promptSha
|
|
11536
|
+
};
|
|
11044
11537
|
try {
|
|
11045
|
-
|
|
11046
|
-
out.push({ role, action: "already_stored" });
|
|
11047
|
-
continue;
|
|
11048
|
-
}
|
|
11049
|
-
if (o.dryRun) {
|
|
11050
|
-
out.push({ role, action: "minted", detail: "dry run" });
|
|
11051
|
-
continue;
|
|
11052
|
-
}
|
|
11053
|
-
const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
11054
|
-
method: "POST",
|
|
11055
|
-
headers: {
|
|
11056
|
-
Authorization: `Bearer ${resolveApiKey(route2)}`,
|
|
11057
|
-
"Content-Type": "application/json",
|
|
11058
|
-
"User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
|
|
11059
|
-
},
|
|
11060
|
-
body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
|
|
11061
|
-
});
|
|
11062
|
-
if (res.status === 403) {
|
|
11063
|
-
out.push({ role, action: "forbidden" });
|
|
11064
|
-
break;
|
|
11065
|
-
}
|
|
11066
|
-
if (!res.ok) {
|
|
11067
|
-
out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
|
|
11068
|
-
continue;
|
|
11069
|
-
}
|
|
11070
|
-
const body = await res.json();
|
|
11071
|
-
if (!body.key) {
|
|
11072
|
-
out.push({ role, action: "failed", detail: "response carried no key" });
|
|
11073
|
-
continue;
|
|
11074
|
-
}
|
|
11538
|
+
const sent = await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, report);
|
|
11075
11539
|
try {
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
revoked = del2.ok;
|
|
11093
|
-
} catch {
|
|
11094
|
-
revoked = false;
|
|
11095
|
-
}
|
|
11540
|
+
clearShipAttention(new State(stateDirOfStream(id.streamPath)), agentLogRefusedKey(id.route));
|
|
11541
|
+
} catch {
|
|
11542
|
+
}
|
|
11543
|
+
if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
|
|
11544
|
+
if (sent.counterRetry) {
|
|
11545
|
+
try {
|
|
11546
|
+
queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
11547
|
+
version: 1,
|
|
11548
|
+
route: id.route,
|
|
11549
|
+
role: id.role,
|
|
11550
|
+
target: id.agentLog,
|
|
11551
|
+
write: sent.counterRetry,
|
|
11552
|
+
attempts: 0,
|
|
11553
|
+
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11554
|
+
});
|
|
11555
|
+
} catch {
|
|
11096
11556
|
}
|
|
11097
|
-
out.push({
|
|
11098
|
-
role,
|
|
11099
|
-
action: "failed",
|
|
11100
|
-
detail: revoked ? `minted but could not be stored (${describeStoreError(e)}); revoked` : `a key for ${role} is live on the server and not stored (${describeStoreError(e)}) \u2014 revoke it from that seat's Agents row`
|
|
11101
|
-
});
|
|
11102
|
-
continue;
|
|
11103
11557
|
}
|
|
11104
|
-
out.push({ role, action: "minted" });
|
|
11105
11558
|
} catch (e) {
|
|
11106
|
-
|
|
11559
|
+
if (e instanceof AgentLogUnreachableError) {
|
|
11560
|
+
try {
|
|
11561
|
+
queueAgentRun(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
11562
|
+
version: 1,
|
|
11563
|
+
route: id.route,
|
|
11564
|
+
role: id.role,
|
|
11565
|
+
target: id.agentLog,
|
|
11566
|
+
report,
|
|
11567
|
+
attempts: 0,
|
|
11568
|
+
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11569
|
+
});
|
|
11570
|
+
emit.warn(`could not report agent log (${e.message}); queued to retry`, { step: "agent" });
|
|
11571
|
+
return { ms: ms3, ticket: attributedTicket, handoffPending: !!pending };
|
|
11572
|
+
} catch {
|
|
11573
|
+
}
|
|
11574
|
+
}
|
|
11575
|
+
if (e instanceof AgentLogHttpError && e.row && e.status >= 400 && e.status < 500) {
|
|
11576
|
+
try {
|
|
11577
|
+
await raiseShipAttention(
|
|
11578
|
+
{ state: new State(stateDirOfStream(id.streamPath)), emit },
|
|
11579
|
+
agentLogRefusedItem(id.route, id.role, e.status)
|
|
11580
|
+
);
|
|
11581
|
+
} catch {
|
|
11582
|
+
}
|
|
11583
|
+
}
|
|
11584
|
+
emit.warn(`could not report agent log: ${e.message}`, { step: "agent" });
|
|
11107
11585
|
}
|
|
11108
11586
|
}
|
|
11109
|
-
return
|
|
11587
|
+
return { ms: ms3, ticket: attributedTicket, handoffPending: !!pending };
|
|
11110
11588
|
}
|
|
11111
|
-
function
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11589
|
+
function spawnAgent(plan, emit, hintMoved) {
|
|
11590
|
+
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
11591
|
+
const runner = runnerFor(plan.runner, plan.sandboxConfig);
|
|
11592
|
+
const started = Date.now();
|
|
11593
|
+
const startedAt = new Date(started).toISOString();
|
|
11594
|
+
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
11595
|
+
const eventsSink = openSink(plan.eventsPath);
|
|
11596
|
+
const id = identityOf(plan, adapter.harness);
|
|
11597
|
+
let streamFd;
|
|
11598
|
+
try {
|
|
11599
|
+
mkdirSync13(dirname9(plan.streamPath), { recursive: true });
|
|
11600
|
+
streamFd = openSync3(plan.streamPath, "a");
|
|
11601
|
+
} catch (e) {
|
|
11602
|
+
void endSink(eventsSink);
|
|
11603
|
+
return Promise.reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
|
|
11123
11604
|
}
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
const doFetch = o.fetchImpl ?? fetch;
|
|
11130
|
-
const targets = [
|
|
11131
|
-
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
11132
|
-
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
11133
|
-
];
|
|
11134
|
-
let store;
|
|
11605
|
+
const failed = (e) => {
|
|
11606
|
+
void endSink(eventsSink);
|
|
11607
|
+
return Promise.reject(new AgentSpawnError(`cannot run ${plan.bin}: ${e.message}`));
|
|
11608
|
+
};
|
|
11609
|
+
let begun;
|
|
11135
11610
|
try {
|
|
11136
|
-
|
|
11611
|
+
begun = runner.start(plan, { env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv }, streamFd, emit });
|
|
11137
11612
|
} catch (e) {
|
|
11138
|
-
|
|
11613
|
+
closeSync3(streamFd);
|
|
11614
|
+
return failed(e);
|
|
11139
11615
|
}
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
const session = await store.get(host, slug2);
|
|
11150
|
-
if (!session?.apiKey) {
|
|
11151
|
-
out.push({ role, action: "not_stored" });
|
|
11152
|
-
continue;
|
|
11153
|
-
}
|
|
11154
|
-
if (!session.keyId) {
|
|
11155
|
-
out.push({ role, action: "failed", detail: "no key id was recorded when this key was minted \u2014 revoke it from the seat's Agents row" });
|
|
11156
|
-
continue;
|
|
11616
|
+
if (begun instanceof Promise) {
|
|
11617
|
+
return begun.then(
|
|
11618
|
+
(run3) => {
|
|
11619
|
+
closeSync3(streamFd);
|
|
11620
|
+
return supervise(run3);
|
|
11621
|
+
},
|
|
11622
|
+
(e) => {
|
|
11623
|
+
closeSync3(streamFd);
|
|
11624
|
+
return failed(e);
|
|
11157
11625
|
}
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11626
|
+
);
|
|
11627
|
+
}
|
|
11628
|
+
closeSync3(streamFd);
|
|
11629
|
+
return supervise(begun);
|
|
11630
|
+
function supervise({ child, handle, promptDelivered }) {
|
|
11631
|
+
return new Promise((resolve7, reject) => {
|
|
11632
|
+
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11633
|
+
const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
|
|
11634
|
+
if (child.pid !== void 0) {
|
|
11635
|
+
const sidecar = {
|
|
11636
|
+
pid: child.pid,
|
|
11637
|
+
ownerPid: process.pid,
|
|
11638
|
+
spawnedAt,
|
|
11639
|
+
role: plan.role,
|
|
11640
|
+
route: plan.route,
|
|
11641
|
+
...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
|
|
11642
|
+
...handle.runner === "sandbox" ? { runner: "sandbox", sandbox: handle.sandbox, sandboxBin: handle.sandboxBin } : {},
|
|
11643
|
+
ticket: plan.ticket ?? null,
|
|
11644
|
+
ticketRecordId: plan.ticketRecordId ?? null,
|
|
11645
|
+
repo: plan.repo ?? null,
|
|
11646
|
+
routeName: plan.routeName,
|
|
11647
|
+
model: plan.model,
|
|
11648
|
+
toolchain: id.toolchain,
|
|
11649
|
+
harness: id.harness,
|
|
11650
|
+
provider: id.provider,
|
|
11651
|
+
eventsPath: plan.eventsPath,
|
|
11652
|
+
agentLog: id.agentLog,
|
|
11653
|
+
promptVersion: plan.promptVersion,
|
|
11654
|
+
promptSha: plan.promptSha
|
|
11655
|
+
};
|
|
11656
|
+
try {
|
|
11657
|
+
writeFileSync15(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
11658
|
+
} catch {
|
|
11659
|
+
}
|
|
11161
11660
|
}
|
|
11162
|
-
const
|
|
11163
|
-
|
|
11164
|
-
|
|
11661
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
11662
|
+
const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
|
|
11663
|
+
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
11664
|
+
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
11665
|
+
const tail3 = new FileTail(plan.streamPath);
|
|
11666
|
+
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
11667
|
+
let killed;
|
|
11668
|
+
const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
|
|
11669
|
+
void killForDeadline(() => runner.kill(handle, WALL_CLOCK_REASON), child.pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
|
|
11670
|
+
killed = k;
|
|
11671
|
+
});
|
|
11672
|
+
}, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
|
|
11673
|
+
let spawnFailed = false;
|
|
11674
|
+
child.on("error", (err3) => {
|
|
11675
|
+
spawnFailed = true;
|
|
11676
|
+
clearInterval(timer);
|
|
11677
|
+
clearTimeout(limitTimer);
|
|
11678
|
+
void runner.release(handle, emit);
|
|
11679
|
+
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
11165
11680
|
});
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
}
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11681
|
+
child.on("close", async (code) => {
|
|
11682
|
+
clearInterval(timer);
|
|
11683
|
+
clearTimeout(limitTimer);
|
|
11684
|
+
obs.feed(tail3.poll());
|
|
11685
|
+
obs.end();
|
|
11686
|
+
await endSink(eventsSink);
|
|
11687
|
+
if (spawnFailed) return;
|
|
11688
|
+
await runner.release(handle, emit);
|
|
11689
|
+
if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
|
|
11690
|
+
killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
|
|
11691
|
+
}
|
|
11692
|
+
const { ms: ms3, handoffPending } = await finishRun(
|
|
11693
|
+
id,
|
|
11694
|
+
obs,
|
|
11695
|
+
emit,
|
|
11696
|
+
startedAt,
|
|
11697
|
+
{ code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
|
|
11698
|
+
plan.agentLog?.apiKey,
|
|
11699
|
+
hintMoved
|
|
11700
|
+
);
|
|
11701
|
+
resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
11702
|
+
});
|
|
11703
|
+
child.stdin?.on("error", (err3) => {
|
|
11704
|
+
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
11705
|
+
});
|
|
11706
|
+
if (promptDelivered) child.stdin?.end();
|
|
11707
|
+
else child.stdin?.end(plan.prompt);
|
|
11708
|
+
});
|
|
11188
11709
|
}
|
|
11189
11710
|
}
|
|
11190
|
-
async function
|
|
11191
|
-
const
|
|
11192
|
-
|
|
11193
|
-
const
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11711
|
+
async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
11712
|
+
const adapter = adapterFor(sc.harness);
|
|
11713
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
11714
|
+
const id = { ...sc, streamPath };
|
|
11715
|
+
const eventsSink = openSink(sc.eventsPath);
|
|
11716
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
11717
|
+
const tail3 = new FileTail(streamPath);
|
|
11718
|
+
obs.muted = true;
|
|
11719
|
+
obs.feed(tail3.poll());
|
|
11720
|
+
obs.muted = false;
|
|
11721
|
+
const runner = runnerOfSidecar(sc);
|
|
11722
|
+
const adopted = isAlive(sc);
|
|
11723
|
+
let killed;
|
|
11724
|
+
if (adopted) {
|
|
11725
|
+
o.onAdopted?.();
|
|
11726
|
+
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
11727
|
+
while (isAlive(sc)) {
|
|
11728
|
+
obs.feed(tail3.poll());
|
|
11729
|
+
if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
|
|
11730
|
+
killed = await killForDeadline(() => runner.kill(runner.reattach(sc), WALL_CLOCK_REASON), sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
|
|
11731
|
+
continue;
|
|
11732
|
+
}
|
|
11733
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
11734
|
+
}
|
|
11203
11735
|
}
|
|
11736
|
+
obs.feed(tail3.poll());
|
|
11737
|
+
obs.end();
|
|
11738
|
+
await endSink(eventsSink);
|
|
11739
|
+
await runner.release(runner.reattach(sc), emit);
|
|
11740
|
+
const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
11741
|
+
const { ms: ms3, ticket } = await finishRun(
|
|
11742
|
+
id,
|
|
11743
|
+
obs,
|
|
11744
|
+
emit,
|
|
11745
|
+
sc.spawnedAt,
|
|
11746
|
+
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
11747
|
+
o.agentLogKey
|
|
11748
|
+
);
|
|
11749
|
+
return { adopted, ...interrupted ? { interrupted } : {}, ...killed ? { killed } : {}, ms: ms3, ticket };
|
|
11204
11750
|
}
|
|
11205
11751
|
|
|
11206
11752
|
// src/selftest.ts
|
|
@@ -11364,7 +11910,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
11364
11910
|
...adapter.runEnv(spec),
|
|
11365
11911
|
[KEY_VAR]: i.apiKey
|
|
11366
11912
|
};
|
|
11367
|
-
const cwd = mkdtempSync2(
|
|
11913
|
+
const cwd = mkdtempSync2(join27(tmpdir4(), "crew-selftest-"));
|
|
11368
11914
|
try {
|
|
11369
11915
|
const run3 = await deps.run({
|
|
11370
11916
|
bin: invocation.bin,
|
|
@@ -11397,11 +11943,11 @@ function formatResult(r) {
|
|
|
11397
11943
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
11398
11944
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
11399
11945
|
}
|
|
11400
|
-
var fileFor2 = (stateDir) =>
|
|
11946
|
+
var fileFor2 = (stateDir) => join27(stateDir, "selftest.json");
|
|
11401
11947
|
function readSelftest(stateDir) {
|
|
11402
11948
|
try {
|
|
11403
11949
|
const p = fileFor2(stateDir);
|
|
11404
|
-
return
|
|
11950
|
+
return existsSync25(p) ? JSON.parse(readFileSync22(p, "utf8")) : {};
|
|
11405
11951
|
} catch {
|
|
11406
11952
|
return {};
|
|
11407
11953
|
}
|
|
@@ -11422,7 +11968,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
11422
11968
|
}
|
|
11423
11969
|
|
|
11424
11970
|
// src/cli.ts
|
|
11425
|
-
import { resolve as resolve6, dirname as
|
|
11971
|
+
import { resolve as resolve6, dirname as dirname26, join as join49, basename as basename15 } from "path";
|
|
11426
11972
|
import { fileURLToPath } from "url";
|
|
11427
11973
|
import { hostname as hostname4, homedir as homedir11 } from "os";
|
|
11428
11974
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
@@ -11619,14 +12165,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
11619
12165
|
}
|
|
11620
12166
|
|
|
11621
12167
|
// src/connect-resume.ts
|
|
11622
|
-
import { mkdirSync as mkdirSync15, readFileSync as
|
|
11623
|
-
import { dirname as
|
|
12168
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync23, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
|
|
12169
|
+
import { dirname as dirname10, join as join28 } from "path";
|
|
11624
12170
|
function lastSessionFile(stateDir) {
|
|
11625
|
-
return
|
|
12171
|
+
return join28(stateDir, "connect-session.json");
|
|
11626
12172
|
}
|
|
11627
12173
|
function readPointers(stateDir) {
|
|
11628
12174
|
try {
|
|
11629
|
-
const parsed = JSON.parse(
|
|
12175
|
+
const parsed = JSON.parse(readFileSync23(lastSessionFile(stateDir), "utf8"));
|
|
11630
12176
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
11631
12177
|
const out = {};
|
|
11632
12178
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -11640,7 +12186,7 @@ function readPointers(stateDir) {
|
|
|
11640
12186
|
}
|
|
11641
12187
|
function writePointers(stateDir, pointers) {
|
|
11642
12188
|
const file = lastSessionFile(stateDir);
|
|
11643
|
-
mkdirSync15(
|
|
12189
|
+
mkdirSync15(dirname10(file), { recursive: true });
|
|
11644
12190
|
writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
|
|
11645
12191
|
`, { mode: 384 });
|
|
11646
12192
|
}
|
|
@@ -11726,15 +12272,15 @@ async function signOutSessions(o) {
|
|
|
11726
12272
|
}
|
|
11727
12273
|
|
|
11728
12274
|
// src/reset.ts
|
|
11729
|
-
import { existsSync as
|
|
11730
|
-
import { dirname as
|
|
12275
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
|
|
12276
|
+
import { dirname as dirname11, join as join29 } from "path";
|
|
11731
12277
|
import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
11732
12278
|
function clonedCheckoutsFile(stateDir) {
|
|
11733
|
-
return
|
|
12279
|
+
return join29(stateDir, "cloned-checkouts.json");
|
|
11734
12280
|
}
|
|
11735
12281
|
function readClonedCheckouts(stateDir) {
|
|
11736
12282
|
try {
|
|
11737
|
-
const parsed = JSON.parse(
|
|
12283
|
+
const parsed = JSON.parse(readFileSync24(clonedCheckoutsFile(stateDir), "utf8"));
|
|
11738
12284
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
11739
12285
|
} catch {
|
|
11740
12286
|
return [];
|
|
@@ -11744,7 +12290,7 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
11744
12290
|
const known = readClonedCheckouts(stateDir);
|
|
11745
12291
|
if (known.includes(dir)) return;
|
|
11746
12292
|
const file = clonedCheckoutsFile(stateDir);
|
|
11747
|
-
mkdirSync16(
|
|
12293
|
+
mkdirSync16(dirname11(file), { recursive: true });
|
|
11748
12294
|
writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
11749
12295
|
`);
|
|
11750
12296
|
}
|
|
@@ -11797,407 +12343,137 @@ async function runReset(o, fx) {
|
|
|
11797
12343
|
if (o.removeCheckouts) {
|
|
11798
12344
|
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
11799
12345
|
}
|
|
11800
|
-
if (o.beforeState) steps.push(...await o.beforeState());
|
|
11801
|
-
steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
|
|
11802
|
-
if (
|
|
11803
|
-
if (!o.dryRun) rmSync16(o.configFile, { force: true });
|
|
11804
|
-
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
11805
|
-
} else {
|
|
11806
|
-
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
11807
|
-
}
|
|
11808
|
-
return steps;
|
|
11809
|
-
}
|
|
11810
|
-
function removeCheckout(dir, dryRun, fx) {
|
|
11811
|
-
if (!
|
|
11812
|
-
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
11813
|
-
if (dirty.length) {
|
|
11814
|
-
return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
|
|
11815
|
-
}
|
|
11816
|
-
if (linkedWorktrees.length) {
|
|
11817
|
-
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
11818
|
-
}
|
|
11819
|
-
if (!dryRun) rmSync16(dir, { recursive: true, force: true });
|
|
11820
|
-
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
11821
|
-
}
|
|
11822
|
-
function removeState(stateDir, dryRun) {
|
|
11823
|
-
const out = [];
|
|
11824
|
-
const targets = [
|
|
11825
|
-
try {
|
|
11826
|
-
for (const name of readdirSync9(stateDir)) {
|
|
11827
|
-
if (name.startsWith(".") && statSync8(join27(stateDir, name)).isFile()) targets.push(join27(stateDir, name));
|
|
11828
|
-
}
|
|
11829
|
-
} catch {
|
|
11830
|
-
}
|
|
11831
|
-
for (const t of targets) {
|
|
11832
|
-
if (!existsSync23(t)) continue;
|
|
11833
|
-
if (!dryRun) rmSync16(t, { recursive: true, force: true });
|
|
11834
|
-
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
11835
|
-
}
|
|
11836
|
-
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
11837
|
-
return out;
|
|
11838
|
-
}
|
|
11839
|
-
function wipeStateDir(stateDir, dryRun) {
|
|
11840
|
-
let names;
|
|
11841
|
-
try {
|
|
11842
|
-
names = readdirSync9(stateDir);
|
|
11843
|
-
} catch {
|
|
11844
|
-
return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
|
|
11845
|
-
}
|
|
11846
|
-
if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
|
|
11847
|
-
if (!dryRun) for (const n of names) rmSync16(join27(stateDir, n), { recursive: true, force: true });
|
|
11848
|
-
return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
|
|
11849
|
-
}
|
|
11850
|
-
function describeResetStep(s) {
|
|
11851
|
-
const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
|
|
11852
|
-
return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
|
|
11853
|
-
}
|
|
11854
|
-
function resetIncomplete(steps) {
|
|
11855
|
-
return steps.some((s) => s.action === "failed" || s.action === "kept");
|
|
11856
|
-
}
|
|
11857
|
-
|
|
11858
|
-
// src/deprovision.ts
|
|
11859
|
-
var mark2 = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
11860
|
-
async function runDeprovision(o, fx, deps) {
|
|
11861
|
-
const attempt = (step, target, fn, dry) => {
|
|
11862
|
-
try {
|
|
11863
|
-
if (!dry) fn();
|
|
11864
|
-
return { step, target, action: mark2(dry) };
|
|
11865
|
-
} catch (e) {
|
|
11866
|
-
return { step, target, action: "failed", detail: e.message };
|
|
11867
|
-
}
|
|
11868
|
-
};
|
|
11869
|
-
const afterScheduler = async () => {
|
|
11870
|
-
const steps2 = [];
|
|
11871
|
-
for (const run3 of deps.liveRuns()) {
|
|
11872
|
-
const target = `${run3.role} run on ${run3.route} (pid ${run3.pid})`;
|
|
11873
|
-
if (o.dryRun) {
|
|
11874
|
-
steps2.push({ step: "run", target, action: "would-remove" });
|
|
11875
|
-
continue;
|
|
11876
|
-
}
|
|
11877
|
-
try {
|
|
11878
|
-
await deps.killRun(run3);
|
|
11879
|
-
deps.recordKill(run3);
|
|
11880
|
-
} catch (e) {
|
|
11881
|
-
steps2.push({ step: "run", target, action: "failed", detail: e.message });
|
|
11882
|
-
continue;
|
|
11883
|
-
}
|
|
11884
|
-
try {
|
|
11885
|
-
await deps.handBack(run3);
|
|
11886
|
-
steps2.push({ step: "run", target, action: "removed", detail: "killed" });
|
|
11887
|
-
} catch (e) {
|
|
11888
|
-
steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
|
|
11889
|
-
}
|
|
11890
|
-
}
|
|
11891
|
-
for (const name of deps.sandboxes()) steps2.push(attempt("sandbox", name, () => {
|
|
11892
|
-
deps.removeSandbox(name);
|
|
11893
|
-
}, o.dryRun));
|
|
11894
|
-
if (!o.dryRun) {
|
|
11895
|
-
for (const r of await deps.clearShipRows()) {
|
|
11896
|
-
steps2.push({ step: "ship-row", target: r.route, action: r.ok ? "removed" : "failed", detail: r.detail });
|
|
11897
|
-
}
|
|
11898
|
-
}
|
|
11899
|
-
return steps2;
|
|
11900
|
-
};
|
|
11901
|
-
const beforeState = async () => {
|
|
11902
|
-
const steps2 = [];
|
|
11903
|
-
for (const host of deps.forgeHosts()) {
|
|
11904
|
-
try {
|
|
11905
|
-
const result = o.dryRun ? "erased" : deps.eraseForgeToken(host);
|
|
11906
|
-
if (result === "env") steps2.push({ step: "forge", target: host, action: "kept", detail: "the token is read from the environment; crew does not own it" });
|
|
11907
|
-
else if (result === "absent") steps2.push({ step: "forge", target: host, action: "absent" });
|
|
11908
|
-
else steps2.push({ step: "forge", target: host, action: mark2(o.dryRun) });
|
|
11909
|
-
} catch (e) {
|
|
11910
|
-
steps2.push({ step: "forge", target: host, action: "failed", detail: e.message });
|
|
11911
|
-
}
|
|
11912
|
-
}
|
|
11913
|
-
const { volumes, images } = deps.labeled();
|
|
11914
|
-
for (const v of volumes) steps2.push(attempt("cache", `volume ${v}`, () => {
|
|
11915
|
-
deps.removeVolume(v);
|
|
11916
|
-
}, o.dryRun));
|
|
11917
|
-
for (const i of images) steps2.push(attempt("cache", `image ${i}`, () => {
|
|
11918
|
-
deps.removeImage(i);
|
|
11919
|
-
}, o.dryRun));
|
|
11920
|
-
return steps2;
|
|
11921
|
-
};
|
|
11922
|
-
const steps = await runReset({ ...o, removeCheckouts: true, wipeState: true, afterScheduler, beforeState }, fx);
|
|
11923
|
-
const remaining = o.dryRun ? [] : deps.remaining();
|
|
11924
|
-
return { steps, remaining, ok: !resetIncomplete(steps) && remaining.length === 0 };
|
|
11925
|
-
}
|
|
11926
|
-
|
|
11927
|
-
// src/deprovision-host.ts
|
|
11928
|
-
import { spawnSync as spawnSync6 } from "child_process";
|
|
11929
|
-
import { existsSync as existsSync26, readdirSync as readdirSync12 } from "fs";
|
|
11930
|
-
|
|
11931
|
-
// src/interrupted.ts
|
|
11932
|
-
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
|
|
11933
|
-
import { join as join28 } from "path";
|
|
11934
|
-
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
11935
|
-
function interruptedComment(b) {
|
|
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.";
|
|
11937
|
-
return `${INTERRUPTED_MARKER}
|
|
11938
|
-
|
|
11939
|
-
The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
|
|
11940
|
-
}
|
|
11941
|
-
var handled = /* @__PURE__ */ new Set();
|
|
11942
|
-
var pidAlive = (pid) => {
|
|
11943
|
-
try {
|
|
11944
|
-
process.kill(pid, 0);
|
|
11945
|
-
return true;
|
|
11946
|
-
} catch (e) {
|
|
11947
|
-
return e.code === "EPERM";
|
|
11948
|
-
}
|
|
11949
|
-
};
|
|
11950
|
-
function runFinished(streamPath) {
|
|
11951
|
-
const path = `${streamPath}.meta.json`;
|
|
11952
|
-
if (!existsSync24(path)) return false;
|
|
11953
|
-
try {
|
|
11954
|
-
return JSON.parse(readFileSync24(path, "utf8")).running !== true;
|
|
11955
|
-
} catch {
|
|
11956
|
-
return true;
|
|
11957
|
-
}
|
|
11958
|
-
}
|
|
11959
|
-
function orphanedStreams(o) {
|
|
11960
|
-
const dir = join28(o.stateDir, "streams");
|
|
11961
|
-
if (!existsSync24(dir)) return [];
|
|
11962
|
-
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
11963
|
-
const out = [];
|
|
11964
|
-
for (const name of readdirSync10(dir)) {
|
|
11965
|
-
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
11966
|
-
const streamPath = join28(dir, name);
|
|
11967
|
-
if (runFinished(streamPath)) continue;
|
|
11968
|
-
const sidecar = readRunSidecar(streamPath);
|
|
11969
|
-
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
11970
|
-
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
11971
|
-
out.push({ streamPath, sidecar });
|
|
11972
|
-
}
|
|
11973
|
-
return out;
|
|
11974
|
-
}
|
|
11975
|
-
async function reconcileRuns(o) {
|
|
11976
|
-
let adopted = 0;
|
|
11977
|
-
let interrupted = 0;
|
|
11978
|
-
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
11979
|
-
if (handled.has(streamPath)) continue;
|
|
11980
|
-
handled.add(streamPath);
|
|
11981
|
-
const isAlive = o.isAlive ?? runIsAlive;
|
|
11982
|
-
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
|
|
11983
|
-
const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
|
|
11984
|
-
const work = (async () => {
|
|
11985
|
-
try {
|
|
11986
|
-
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
11987
|
-
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
11988
|
-
isAlive: o.isAlive,
|
|
11989
|
-
pollMs: o.pollMs,
|
|
11990
|
-
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
11991
|
-
});
|
|
11992
|
-
if (result.adopted) adopted++;
|
|
11993
|
-
else interrupted++;
|
|
11994
|
-
try {
|
|
11995
|
-
await o.afterRun?.(sidecar, result);
|
|
11996
|
-
} catch (e) {
|
|
11997
|
-
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
11998
|
-
}
|
|
11999
|
-
} catch (e) {
|
|
12000
|
-
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
12001
|
-
} finally {
|
|
12002
|
-
if (lock?.ok) lock.release();
|
|
12003
|
-
if (ticketLock?.ok) ticketLock.release();
|
|
12004
|
-
}
|
|
12005
|
-
})();
|
|
12006
|
-
if (isAlive(sidecar)) {
|
|
12007
|
-
if (o.inFlight) {
|
|
12008
|
-
o.inFlight.add(work);
|
|
12009
|
-
void work.finally(() => o.inFlight.delete(work));
|
|
12010
|
-
}
|
|
12011
|
-
} else {
|
|
12012
|
-
await work;
|
|
12013
|
-
}
|
|
12014
|
-
}
|
|
12015
|
-
return { adopted, interrupted };
|
|
12016
|
-
}
|
|
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));
|
|
12034
|
-
}
|
|
12035
|
-
function siblingRuns(runs, ownTicketRecordId) {
|
|
12036
|
-
return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
|
|
12037
|
-
}
|
|
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" });
|
|
12051
|
-
}
|
|
12052
|
-
}
|
|
12346
|
+
if (o.beforeState) steps.push(...await o.beforeState());
|
|
12347
|
+
steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
|
|
12348
|
+
if (existsSync26(o.configFile)) {
|
|
12349
|
+
if (!o.dryRun) rmSync16(o.configFile, { force: true });
|
|
12350
|
+
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
12351
|
+
} else {
|
|
12352
|
+
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
12353
|
+
}
|
|
12354
|
+
return steps;
|
|
12355
|
+
}
|
|
12356
|
+
function removeCheckout(dir, dryRun, fx) {
|
|
12357
|
+
if (!existsSync26(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
12358
|
+
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
12359
|
+
if (dirty.length) {
|
|
12360
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
|
|
12361
|
+
}
|
|
12362
|
+
if (linkedWorktrees.length) {
|
|
12363
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
12364
|
+
}
|
|
12365
|
+
if (!dryRun) rmSync16(dir, { recursive: true, force: true });
|
|
12366
|
+
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
12367
|
+
}
|
|
12368
|
+
function removeState(stateDir, dryRun) {
|
|
12369
|
+
const out = [];
|
|
12370
|
+
const targets = [join29(stateDir, "resolved"), join29(stateDir, "keys"), clonedCheckoutsFile(stateDir), join29(stateDir, "connect-session.json"), join29(stateDir, "reload-requested"), join29(stateDir, "daemon.started")];
|
|
12053
12371
|
try {
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
|
|
12057
|
-
} else {
|
|
12058
|
-
await tracker.beatIdle(o.shipName);
|
|
12372
|
+
for (const name of readdirSync12(stateDir)) {
|
|
12373
|
+
if (name.startsWith(".") && statSync8(join29(stateDir, name)).isFile()) targets.push(join29(stateDir, name));
|
|
12059
12374
|
}
|
|
12060
|
-
} catch
|
|
12061
|
-
|
|
12375
|
+
} catch {
|
|
12376
|
+
}
|
|
12377
|
+
for (const t of targets) {
|
|
12378
|
+
if (!existsSync26(t)) continue;
|
|
12379
|
+
if (!dryRun) rmSync16(t, { recursive: true, force: true });
|
|
12380
|
+
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
12062
12381
|
}
|
|
12382
|
+
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
12383
|
+
return out;
|
|
12063
12384
|
}
|
|
12064
|
-
function
|
|
12385
|
+
function wipeStateDir(stateDir, dryRun) {
|
|
12386
|
+
let names;
|
|
12065
12387
|
try {
|
|
12066
|
-
|
|
12388
|
+
names = readdirSync12(stateDir);
|
|
12067
12389
|
} catch {
|
|
12068
|
-
return
|
|
12390
|
+
return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
|
|
12069
12391
|
}
|
|
12070
|
-
|
|
12071
|
-
const
|
|
12072
|
-
|
|
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
|
|
12085
|
-
};
|
|
12392
|
+
if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
|
|
12393
|
+
if (!dryRun) for (const n of names) rmSync16(join29(stateDir, n), { recursive: true, force: true });
|
|
12394
|
+
return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
|
|
12086
12395
|
}
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
var DOCKER_LABEL_CACHE = "crew.kind=cache";
|
|
12091
|
-
var DOCKER_LABEL_REPO_LAYER = "crew.layer=repo";
|
|
12092
|
-
function docker(args) {
|
|
12093
|
-
const res = spawnSync6("docker", args, { encoding: "utf8" });
|
|
12094
|
-
if (res.error || res.status !== 0) throw new Error(`docker ${args[0]} ${args[1] ?? ""} failed: ${(res.stderr || String(res.error ?? "")).trim().split("\n").pop()}`);
|
|
12095
|
-
return res.stdout;
|
|
12396
|
+
function describeResetStep(s) {
|
|
12397
|
+
const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
|
|
12398
|
+
return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
|
|
12096
12399
|
}
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
if (!dockerAvailable()) return { volumes: [], images: [] };
|
|
12100
|
-
return {
|
|
12101
|
-
volumes: lines(docker(["volume", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_CACHE}`])),
|
|
12102
|
-
images: lines(docker(["image", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_REPO_LAYER}`]))
|
|
12103
|
-
};
|
|
12400
|
+
function resetIncomplete(steps) {
|
|
12401
|
+
return steps.some((s) => s.action === "failed" || s.action === "kept");
|
|
12104
12402
|
}
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
const
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12403
|
+
|
|
12404
|
+
// src/deprovision.ts
|
|
12405
|
+
var mark2 = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
12406
|
+
async function runDeprovision(o, fx, deps) {
|
|
12407
|
+
const attempt = (step, target, fn, dry) => {
|
|
12408
|
+
try {
|
|
12409
|
+
if (!dry) fn();
|
|
12410
|
+
return { step, target, action: mark2(dry) };
|
|
12411
|
+
} catch (e) {
|
|
12412
|
+
return { step, target, action: "failed", detail: e.message };
|
|
12413
|
+
}
|
|
12116
12414
|
};
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
step: "
|
|
12123
|
-
|
|
12124
|
-
data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
|
|
12125
|
-
});
|
|
12126
|
-
},
|
|
12127
|
-
async handBack(run3) {
|
|
12128
|
-
const route2 = routeFor(run3.route);
|
|
12129
|
-
if (!run3.ticketRecordId || !route2) return;
|
|
12130
|
-
const memberId = route2.resolved?.seats[run3.role];
|
|
12131
|
-
if (!memberId) return;
|
|
12132
|
-
const identity = await resolveSeatKey(route2, run3.role, getStore);
|
|
12133
|
-
const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
|
|
12134
|
-
const t = await tracker.ticket(run3.ticketRecordId);
|
|
12135
|
-
const target = repoTargetFor(route2, t.repo_id);
|
|
12136
|
-
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
12137
|
-
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
12138
|
-
let dirty = null;
|
|
12139
|
-
try {
|
|
12140
|
-
if (wt) dirty = status(wt.path).length > 0;
|
|
12141
|
-
} catch {
|
|
12415
|
+
const afterScheduler = async () => {
|
|
12416
|
+
const steps2 = [];
|
|
12417
|
+
for (const run3 of deps.liveRuns()) {
|
|
12418
|
+
const target = `${run3.role} run on ${run3.route} (pid ${run3.pid})`;
|
|
12419
|
+
if (o.dryRun) {
|
|
12420
|
+
steps2.push({ step: "run", target, action: "would-remove" });
|
|
12421
|
+
continue;
|
|
12142
12422
|
}
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
sandboxes,
|
|
12150
|
-
removeSandbox: (name) => {
|
|
12151
|
-
stopContainer(name);
|
|
12152
|
-
},
|
|
12153
|
-
async clearShipRows() {
|
|
12154
|
-
const out = [];
|
|
12155
|
-
for (const r of cfg.routes.filter((x) => x.resolved?.shipsModelId)) {
|
|
12156
|
-
try {
|
|
12157
|
-
const found = await new Tracker(r, cfg.ship).clearForRelease(cfg.ship.name);
|
|
12158
|
-
out.push({ route: r.route, ok: true, detail: found ? "cleared engaged, tunnel_status, relay_url" : "no Ships row for this ship" });
|
|
12159
|
-
} catch (e) {
|
|
12160
|
-
out.push({ route: r.route, ok: false, detail: e.message });
|
|
12161
|
-
}
|
|
12423
|
+
try {
|
|
12424
|
+
await deps.killRun(run3);
|
|
12425
|
+
deps.recordKill(run3);
|
|
12426
|
+
} catch (e) {
|
|
12427
|
+
steps2.push({ step: "run", target, action: "failed", detail: e.message });
|
|
12428
|
+
continue;
|
|
12162
12429
|
}
|
|
12163
|
-
return out;
|
|
12164
|
-
},
|
|
12165
|
-
forgeHosts,
|
|
12166
|
-
eraseForgeToken: (host) => eraseForgeToken(cfg.ship.forges, host, stateDir),
|
|
12167
|
-
labeled: labeledDocker,
|
|
12168
|
-
removeVolume: (name) => {
|
|
12169
|
-
docker(["volume", "rm", "-f", name]);
|
|
12170
|
-
},
|
|
12171
|
-
removeImage: (name) => {
|
|
12172
|
-
docker(["image", "rm", "-f", name]);
|
|
12173
|
-
},
|
|
12174
|
-
remaining() {
|
|
12175
|
-
const left = [];
|
|
12176
|
-
if (existsSync26(cfg.configFile)) left.push(`config ${cfg.configFile}`);
|
|
12177
|
-
for (const h of forgeHosts()) if (hasForgeToken(cfg.ship.forges, h, stateDir)) left.push(`forge token for ${h}`);
|
|
12178
12430
|
try {
|
|
12179
|
-
|
|
12180
|
-
|
|
12431
|
+
await deps.handBack(run3);
|
|
12432
|
+
steps2.push({ step: "run", target, action: "removed", detail: "killed" });
|
|
12433
|
+
} catch (e) {
|
|
12434
|
+
steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
|
|
12181
12435
|
}
|
|
12182
|
-
|
|
12436
|
+
}
|
|
12437
|
+
for (const name of deps.sandboxes()) steps2.push(attempt("sandbox", name, () => {
|
|
12438
|
+
deps.removeSandbox(name);
|
|
12439
|
+
}, o.dryRun));
|
|
12440
|
+
if (!o.dryRun) {
|
|
12441
|
+
for (const r of await deps.clearShipRows()) {
|
|
12442
|
+
steps2.push({ step: "ship-row", target: r.route, action: r.ok ? "removed" : "failed", detail: r.detail });
|
|
12443
|
+
}
|
|
12444
|
+
}
|
|
12445
|
+
return steps2;
|
|
12446
|
+
};
|
|
12447
|
+
const beforeState = async () => {
|
|
12448
|
+
const steps2 = [];
|
|
12449
|
+
for (const host of deps.forgeHosts()) {
|
|
12183
12450
|
try {
|
|
12184
|
-
const
|
|
12185
|
-
|
|
12186
|
-
|
|
12451
|
+
const result = o.dryRun ? "erased" : deps.eraseForgeToken(host);
|
|
12452
|
+
if (result === "env") steps2.push({ step: "forge", target: host, action: "kept", detail: "the token is read from the environment; crew does not own it" });
|
|
12453
|
+
else if (result === "absent") steps2.push({ step: "forge", target: host, action: "absent" });
|
|
12454
|
+
else steps2.push({ step: "forge", target: host, action: mark2(o.dryRun) });
|
|
12187
12455
|
} catch (e) {
|
|
12188
|
-
|
|
12456
|
+
steps2.push({ step: "forge", target: host, action: "failed", detail: e.message });
|
|
12189
12457
|
}
|
|
12190
|
-
for (const u of unitPaths()) left.push(`unit ${u}`);
|
|
12191
|
-
if (loadedCrewLabels().length) left.push(`loaded units ${loadedCrewLabels().join(", ")}`);
|
|
12192
|
-
return left;
|
|
12193
12458
|
}
|
|
12459
|
+
const { volumes, images } = deps.labeled();
|
|
12460
|
+
for (const v of volumes) steps2.push(attempt("cache", `volume ${v}`, () => {
|
|
12461
|
+
deps.removeVolume(v);
|
|
12462
|
+
}, o.dryRun));
|
|
12463
|
+
for (const i of images) steps2.push(attempt("cache", `image ${i}`, () => {
|
|
12464
|
+
deps.removeImage(i);
|
|
12465
|
+
}, o.dryRun));
|
|
12466
|
+
return steps2;
|
|
12194
12467
|
};
|
|
12468
|
+
const steps = await runReset({ ...o, removeCheckouts: true, wipeState: true, afterScheduler, beforeState }, fx);
|
|
12469
|
+
const remaining = o.dryRun ? [] : deps.remaining();
|
|
12470
|
+
return { steps, remaining, ok: !resetIncomplete(steps) && remaining.length === 0 };
|
|
12195
12471
|
}
|
|
12196
12472
|
|
|
12197
12473
|
// src/pool-member.ts
|
|
12198
12474
|
import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
|
|
12199
12475
|
import { homedir as homedir8 } from "os";
|
|
12200
|
-
import { dirname as
|
|
12476
|
+
import { dirname as dirname12, join as join30 } from "path";
|
|
12201
12477
|
var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
|
|
12202
12478
|
function backoffMs(attempt) {
|
|
12203
12479
|
return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
|
|
@@ -12399,7 +12675,7 @@ function fileMemberStore(dir) {
|
|
|
12399
12675
|
}
|
|
12400
12676
|
},
|
|
12401
12677
|
write(f) {
|
|
12402
|
-
mkdirSync17(
|
|
12678
|
+
mkdirSync17(dirname12(file), { recursive: true, mode: 448 });
|
|
12403
12679
|
const tmp = `${file}.tmp`;
|
|
12404
12680
|
writeFileSync19(tmp, `${JSON.stringify(f)}
|
|
12405
12681
|
`, { mode: 384 });
|
|
@@ -12457,10 +12733,10 @@ import { existsSync as existsSync30 } from "fs";
|
|
|
12457
12733
|
import { hostname as hostname3 } from "os";
|
|
12458
12734
|
|
|
12459
12735
|
// src/machine-id.ts
|
|
12460
|
-
import { execFileSync as
|
|
12736
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
12461
12737
|
import { createHash as createHash7 } from "crypto";
|
|
12462
12738
|
import { existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
|
|
12463
|
-
import { dirname as
|
|
12739
|
+
import { dirname as dirname13, join as join31 } from "path";
|
|
12464
12740
|
function machineIdPathFor(stateDir) {
|
|
12465
12741
|
return join31(stateDir, "machine-id");
|
|
12466
12742
|
}
|
|
@@ -12468,7 +12744,7 @@ var sha256 = (s) => createHash7("sha256").update(s).digest("hex");
|
|
|
12468
12744
|
function readRawMachineId(platform2 = process.platform) {
|
|
12469
12745
|
try {
|
|
12470
12746
|
if (platform2 === "darwin") {
|
|
12471
|
-
const out =
|
|
12747
|
+
const out = execFileSync8("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
12472
12748
|
encoding: "utf8",
|
|
12473
12749
|
stdio: ["ignore", "pipe", "ignore"]
|
|
12474
12750
|
});
|
|
@@ -12494,7 +12770,7 @@ function ensureMachineId(stateDir, opts = {}) {
|
|
|
12494
12770
|
const raw = (opts.read ?? readRawMachineId)();
|
|
12495
12771
|
if (!raw) return void 0;
|
|
12496
12772
|
const hashed = sha256(raw);
|
|
12497
|
-
mkdirSync18(
|
|
12773
|
+
mkdirSync18(dirname13(path), { recursive: true });
|
|
12498
12774
|
writeFileSync20(path, `${hashed}
|
|
12499
12775
|
`, { mode: 384 });
|
|
12500
12776
|
return hashed;
|
|
@@ -14690,7 +14966,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
|
|
|
14690
14966
|
|
|
14691
14967
|
// src/repo-refresh.ts
|
|
14692
14968
|
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
|
|
14693
|
-
import { dirname as
|
|
14969
|
+
import { dirname as dirname15 } from "path";
|
|
14694
14970
|
var REFRESH_COOLDOWN_MS = 2 * 6e4;
|
|
14695
14971
|
var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
|
|
14696
14972
|
var lastAsked = /* @__PURE__ */ new Map();
|
|
@@ -14722,7 +14998,7 @@ function persistRepoRows(stateDir, routeName, rows) {
|
|
|
14722
14998
|
if (!existsSync31(path)) return;
|
|
14723
14999
|
const raw = JSON.parse(readFileSync29(path, "utf8"));
|
|
14724
15000
|
applyRepoRows(raw, rows);
|
|
14725
|
-
mkdirSync20(
|
|
15001
|
+
mkdirSync20(dirname15(path), { recursive: true });
|
|
14726
15002
|
writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
|
|
14727
15003
|
`);
|
|
14728
15004
|
}
|
|
@@ -15173,7 +15449,7 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
15173
15449
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
15174
15450
|
|
|
15175
15451
|
// src/connect-wizard.ts
|
|
15176
|
-
import { delimiter, dirname as
|
|
15452
|
+
import { delimiter, dirname as dirname16, join as join35 } from "path";
|
|
15177
15453
|
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync25 } from "fs";
|
|
15178
15454
|
import { stringify as stringify2 } from "yaml";
|
|
15179
15455
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
@@ -15222,9 +15498,9 @@ async function runWizard(p, env) {
|
|
|
15222
15498
|
if (entered && isExec(entered)) agentBin = entered;
|
|
15223
15499
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
15224
15500
|
}
|
|
15225
|
-
const nodeDir =
|
|
15501
|
+
const nodeDir = dirname16(env.nodePath);
|
|
15226
15502
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
15227
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
15503
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname16(pnpmPath) : void 0];
|
|
15228
15504
|
p.say(
|
|
15229
15505
|
`
|
|
15230
15506
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -15235,7 +15511,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
15235
15511
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
15236
15512
|
}
|
|
15237
15513
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
15238
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
15514
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname16(dockerPath) : void 0]);
|
|
15239
15515
|
if (extraPath) {
|
|
15240
15516
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
15241
15517
|
}
|
|
@@ -15257,7 +15533,7 @@ routes:
|
|
|
15257
15533
|
${routeBlock}`;
|
|
15258
15534
|
}
|
|
15259
15535
|
function writeNewConfig(path, text2) {
|
|
15260
|
-
mkdirSync21(
|
|
15536
|
+
mkdirSync21(dirname16(path), { recursive: true });
|
|
15261
15537
|
try {
|
|
15262
15538
|
writeFileSync25(path, text2, { mode: 384, flag: "wx" });
|
|
15263
15539
|
return true;
|
|
@@ -15293,7 +15569,7 @@ Wrote ${o.configPath}.
|
|
|
15293
15569
|
// src/connect-json.ts
|
|
15294
15570
|
import { accessSync as accessSync4, constants as constants4 } from "fs";
|
|
15295
15571
|
import { homedir as homedir10 } from "os";
|
|
15296
|
-
import { dirname as
|
|
15572
|
+
import { dirname as dirname17, join as join36 } from "path";
|
|
15297
15573
|
import { delimiter as delimiter2 } from "path";
|
|
15298
15574
|
var CHOICE_EXIT_CODE = 3;
|
|
15299
15575
|
function createEmitter(json2, write) {
|
|
@@ -15359,7 +15635,7 @@ function flagWizardAnswers(f, env) {
|
|
|
15359
15635
|
const agentBin = f.agentBin ?? found?.bin;
|
|
15360
15636
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
15361
15637
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
15362
|
-
const extraPath = f.extraPath ?? deriveExtraPath([
|
|
15638
|
+
const extraPath = f.extraPath ?? deriveExtraPath([dirname17(env.nodePath), pnpmPath ? dirname17(pnpmPath) : void 0, dockerPath ? dirname17(dockerPath) : void 0]);
|
|
15363
15639
|
return {
|
|
15364
15640
|
answers: {
|
|
15365
15641
|
name: f.shipName?.trim() || env.hostname,
|
|
@@ -15374,7 +15650,7 @@ function flagWizardAnswers(f, env) {
|
|
|
15374
15650
|
|
|
15375
15651
|
// src/release-run.ts
|
|
15376
15652
|
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
|
|
15653
|
+
import { dirname as dirname18, join as join37 } from "path";
|
|
15378
15654
|
import { tmpdir as tmpdir5 } from "os";
|
|
15379
15655
|
|
|
15380
15656
|
// src/release.ts
|
|
@@ -15858,11 +16134,11 @@ ${h.prFailure.tail}
|
|
|
15858
16134
|
emit.warn(`could not move to ${reviewing}: ${e.message}`, { ticket: t.issue_id });
|
|
15859
16135
|
}
|
|
15860
16136
|
}
|
|
15861
|
-
for (const
|
|
15862
|
-
const t =
|
|
15863
|
-
const body = `The branch is no longer what was verified: QA verified ${
|
|
16137
|
+
for (const q2 of outcome.requeues) {
|
|
16138
|
+
const t = q2.ticket;
|
|
16139
|
+
const body = `The branch is no longer what was verified: QA verified ${q2.verified.slice(0, 8)}, the branch is now at ${q2.head.slice(0, 8)}. Returned to ${contract.statuses.handoff} for QA to check what changed.`;
|
|
15864
16140
|
if (dryRun) {
|
|
15865
|
-
emit.emit(`would return to ${contract.statuses.handoff} \u2014 branch moved past ${
|
|
16141
|
+
emit.emit(`would return to ${contract.statuses.handoff} \u2014 branch moved past ${q2.verified.slice(0, 8)}`, { ticket: t.issue_id });
|
|
15866
16142
|
continue;
|
|
15867
16143
|
}
|
|
15868
16144
|
try {
|
|
@@ -15870,11 +16146,11 @@ ${h.prFailure.tail}
|
|
|
15870
16146
|
await tracker.postEvent(t.id, body, seat, "requeued");
|
|
15871
16147
|
await tracker.postEvent(
|
|
15872
16148
|
t.id,
|
|
15873
|
-
pushedEventBody(
|
|
16149
|
+
pushedEventBody(q2.head, remote, q2.branch),
|
|
15874
16150
|
seat,
|
|
15875
16151
|
"pushed"
|
|
15876
16152
|
);
|
|
15877
|
-
emit.emit(`returned to ${contract.statuses.handoff} \u2014 branch moved past ${
|
|
16153
|
+
emit.emit(`returned to ${contract.statuses.handoff} \u2014 branch moved past ${q2.verified.slice(0, 8)}`, { ticket: t.issue_id });
|
|
15878
16154
|
} catch (e) {
|
|
15879
16155
|
emit.warn(`could not return to ${contract.statuses.handoff}: ${e.message}`, { ticket: t.issue_id });
|
|
15880
16156
|
}
|
|
@@ -16260,7 +16536,7 @@ function openReleaseCheckout(o) {
|
|
|
16260
16536
|
for (const f of o.repo.worktrees.copy) {
|
|
16261
16537
|
const src = join37(o.cwd, f);
|
|
16262
16538
|
if (!existsSync33(src)) continue;
|
|
16263
|
-
mkdirSync22(
|
|
16539
|
+
mkdirSync22(dirname18(join37(dir, f)), { recursive: true });
|
|
16264
16540
|
copyFileSync(src, join37(dir, f));
|
|
16265
16541
|
}
|
|
16266
16542
|
return { dir, branch, close };
|
|
@@ -17071,8 +17347,8 @@ async function buildWhoami(routes, deps = {}) {
|
|
|
17071
17347
|
const credential = credentialSource(route2);
|
|
17072
17348
|
try {
|
|
17073
17349
|
const key = resolveApiKey(route2);
|
|
17074
|
-
const
|
|
17075
|
-
const res = await doFetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me${
|
|
17350
|
+
const q2 = route2.resolved ? `?workspaceId=${encodeURIComponent(route2.resolved.workspaceId)}` : "";
|
|
17351
|
+
const res = await doFetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me${q2}`, {
|
|
17076
17352
|
headers: { Authorization: `Bearer ${key}`, "User-Agent": deps.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
17077
17353
|
});
|
|
17078
17354
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -17417,9 +17693,9 @@ function startWatch(o) {
|
|
|
17417
17693
|
|
|
17418
17694
|
// src/inbox.ts
|
|
17419
17695
|
function matchMembers(rows, query) {
|
|
17420
|
-
const
|
|
17421
|
-
const asEmail =
|
|
17422
|
-
return rows.filter((r) => asEmail ? (r.email ?? "").toLowerCase() ===
|
|
17696
|
+
const q2 = query.trim().toLowerCase();
|
|
17697
|
+
const asEmail = q2.includes("@");
|
|
17698
|
+
return rows.filter((r) => asEmail ? (r.email ?? "").toLowerCase() === q2 : (r.name ?? "").toLowerCase().includes(q2)).map((r) => ({ id: r.id, name: r.name ?? "", email: r.email ?? "", by: asEmail ? "email" : "name" }));
|
|
17423
17699
|
}
|
|
17424
17700
|
async function gatherInbox(routes, ship, opts = {}) {
|
|
17425
17701
|
const items = [];
|
|
@@ -17580,7 +17856,7 @@ function nextRoles(decision, ran) {
|
|
|
17580
17856
|
|
|
17581
17857
|
// src/ship-name.ts
|
|
17582
17858
|
import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
|
|
17583
|
-
import { dirname as
|
|
17859
|
+
import { dirname as dirname19 } from "path";
|
|
17584
17860
|
import { parseDocument as parseDocument5 } from "yaml";
|
|
17585
17861
|
var ShipNameError = class extends Error {
|
|
17586
17862
|
};
|
|
@@ -17659,7 +17935,7 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
17659
17935
|
}
|
|
17660
17936
|
if (raw.shipName === name) return;
|
|
17661
17937
|
raw.shipName = name;
|
|
17662
|
-
mkdirSync23(
|
|
17938
|
+
mkdirSync23(dirname19(resolvedFile), { recursive: true });
|
|
17663
17939
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
17664
17940
|
writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
|
|
17665
17941
|
`);
|
|
@@ -18353,7 +18629,7 @@ function describeCapability(i) {
|
|
|
18353
18629
|
|
|
18354
18630
|
// src/agents-sync-run.ts
|
|
18355
18631
|
import { existsSync as existsSync36, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
|
|
18356
|
-
import { dirname as
|
|
18632
|
+
import { dirname as dirname20 } from "path";
|
|
18357
18633
|
async function runAgentsSync(target, o) {
|
|
18358
18634
|
const dryRun = o.dryRun ?? false;
|
|
18359
18635
|
const force = o.force ?? false;
|
|
@@ -18390,7 +18666,7 @@ async function runAgentsSync(target, o) {
|
|
|
18390
18666
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
18391
18667
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
18392
18668
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
18393
|
-
mkdirSync24(
|
|
18669
|
+
mkdirSync24(dirname20(resolvedPath), { recursive: true });
|
|
18394
18670
|
writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
18395
18671
|
`);
|
|
18396
18672
|
if (target.resolved) {
|
|
@@ -18811,7 +19087,7 @@ function existingRepoConfig(f) {
|
|
|
18811
19087
|
}
|
|
18812
19088
|
|
|
18813
19089
|
// src/repos-hooks-agent.ts
|
|
18814
|
-
import { spawn as spawn7, execFileSync as
|
|
19090
|
+
import { spawn as spawn7, execFileSync as execFileSync9 } from "child_process";
|
|
18815
19091
|
import { realpathSync as realpathSync2 } from "fs";
|
|
18816
19092
|
import { isAbsolute as isAbsolute2, relative } from "path";
|
|
18817
19093
|
var AgentSuggestError = class extends Error {
|
|
@@ -18914,7 +19190,7 @@ function classifyFailure(o) {
|
|
|
18914
19190
|
}
|
|
18915
19191
|
function porcelain(dir) {
|
|
18916
19192
|
try {
|
|
18917
|
-
return
|
|
19193
|
+
return execFileSync9("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
|
|
18918
19194
|
} catch {
|
|
18919
19195
|
return void 0;
|
|
18920
19196
|
}
|
|
@@ -18983,7 +19259,7 @@ ${run3.err}` });
|
|
|
18983
19259
|
}
|
|
18984
19260
|
|
|
18985
19261
|
// src/sample-ticket.ts
|
|
18986
|
-
import { basename as basename11, dirname as
|
|
19262
|
+
import { basename as basename11, dirname as dirname21, join as join38 } from "path";
|
|
18987
19263
|
var SAMPLE_REPO = "crew-sample";
|
|
18988
19264
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
18989
19265
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -19006,7 +19282,7 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
19006
19282
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
19007
19283
|
`;
|
|
19008
19284
|
function sampleDir(existing, reposBase, workspace) {
|
|
19009
|
-
if (existing.length > 0) return join38(
|
|
19285
|
+
if (existing.length > 0) return join38(dirname21(existing[0]), SAMPLE_REPO);
|
|
19010
19286
|
return join38(reposBase, workspace, SAMPLE_REPO);
|
|
19011
19287
|
}
|
|
19012
19288
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
@@ -19052,7 +19328,7 @@ function planSampleTicket(i) {
|
|
|
19052
19328
|
|
|
19053
19329
|
// src/worktree-sweep.ts
|
|
19054
19330
|
import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync14 } from "fs";
|
|
19055
|
-
import { basename as basename12, dirname as
|
|
19331
|
+
import { basename as basename12, dirname as dirname22, join as join39, resolve as resolve4 } from "path";
|
|
19056
19332
|
function planWorktreeSweep(target, tickets, contract) {
|
|
19057
19333
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
19058
19334
|
const verified = contract.statuses.verified;
|
|
@@ -19116,7 +19392,7 @@ function worktreeTicketNumbers(target) {
|
|
|
19116
19392
|
return [...nums];
|
|
19117
19393
|
}
|
|
19118
19394
|
function siblingWorktreeDirs(target) {
|
|
19119
|
-
const parent =
|
|
19395
|
+
const parent = dirname22(target.dir.replace(/[/\\]+$/, ""));
|
|
19120
19396
|
const stem = `${basename12(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
19121
19397
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
19122
19398
|
let names;
|
|
@@ -19809,14 +20085,14 @@ ${tail2(preview.output)}`);
|
|
|
19809
20085
|
}
|
|
19810
20086
|
|
|
19811
20087
|
// src/periscope-idle.ts
|
|
19812
|
-
import { execFileSync as
|
|
20088
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
19813
20089
|
import { statSync as statSync11 } from "fs";
|
|
19814
20090
|
import { join as join42 } from "path";
|
|
19815
20091
|
var MAX_SCANNED_FILES = 2e4;
|
|
19816
20092
|
function newestWorktreeMtime(root) {
|
|
19817
20093
|
let listing;
|
|
19818
20094
|
try {
|
|
19819
|
-
listing =
|
|
20095
|
+
listing = execFileSync10("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
19820
20096
|
cwd: root,
|
|
19821
20097
|
encoding: "utf8",
|
|
19822
20098
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -19906,9 +20182,9 @@ async function runPeriscopeIdlePass(d) {
|
|
|
19906
20182
|
import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
19907
20183
|
|
|
19908
20184
|
// src/ssh-keys.ts
|
|
19909
|
-
import { execFileSync as
|
|
20185
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
19910
20186
|
import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
|
|
19911
|
-
import { dirname as
|
|
20187
|
+
import { dirname as dirname23, join as join43 } from "path";
|
|
19912
20188
|
var SshKeyError = class extends Error {
|
|
19913
20189
|
};
|
|
19914
20190
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -19918,9 +20194,9 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
19918
20194
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
19919
20195
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
19920
20196
|
if (!existsSync40(privateKeyPath)) {
|
|
19921
|
-
mkdirSync26(
|
|
20197
|
+
mkdirSync26(dirname23(privateKeyPath), { recursive: true });
|
|
19922
20198
|
try {
|
|
19923
|
-
|
|
20199
|
+
execFileSync11("ssh-keygen", [
|
|
19924
20200
|
"-t",
|
|
19925
20201
|
"ed25519",
|
|
19926
20202
|
"-N",
|
|
@@ -19946,7 +20222,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
19946
20222
|
}
|
|
19947
20223
|
function sshKeygenAvailable() {
|
|
19948
20224
|
try {
|
|
19949
|
-
|
|
20225
|
+
execFileSync11("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
19950
20226
|
return true;
|
|
19951
20227
|
} catch (e) {
|
|
19952
20228
|
return e.code !== "ENOENT";
|
|
@@ -20507,6 +20783,13 @@ async function runOnePassUnguarded(o) {
|
|
|
20507
20783
|
const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
|
|
20508
20784
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
20509
20785
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
20786
|
+
if (o.sandboxCache) {
|
|
20787
|
+
try {
|
|
20788
|
+
await o.sandboxCache();
|
|
20789
|
+
} catch (e) {
|
|
20790
|
+
emit.warn(`sandbox cache refresh failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
20791
|
+
}
|
|
20792
|
+
}
|
|
20510
20793
|
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
20511
20794
|
try {
|
|
20512
20795
|
await (o.reconcile ?? reconcileForRoute)(ctx, o);
|
|
@@ -21009,8 +21292,8 @@ import {
|
|
|
21009
21292
|
} from "fs";
|
|
21010
21293
|
import { createConnection } from "net";
|
|
21011
21294
|
import { createServer } from "http";
|
|
21012
|
-
import { basename as basename13, dirname as
|
|
21013
|
-
import { execFile as
|
|
21295
|
+
import { basename as basename13, dirname as dirname24, join as join46 } from "path";
|
|
21296
|
+
import { execFile as execFile3, spawn as spawn10 } from "child_process";
|
|
21014
21297
|
|
|
21015
21298
|
// src/periscope-control.ts
|
|
21016
21299
|
import { spawn as spawn9 } from "child_process";
|
|
@@ -21097,23 +21380,23 @@ async function execute(h, args, streaming, after) {
|
|
|
21097
21380
|
};
|
|
21098
21381
|
}
|
|
21099
21382
|
var isPeriscopePath = (p) => p === "/periscopes" || p.startsWith("/periscopes/");
|
|
21100
|
-
async function handlePeriscopes(h,
|
|
21383
|
+
async function handlePeriscopes(h, q2) {
|
|
21101
21384
|
if (!h || !h.routes.some((r) => r.hostPeriscopes)) return err(404, "not found");
|
|
21102
|
-
if (!
|
|
21103
|
-
const down = /^\/periscopes\/([^/]+)\/down$/.exec(
|
|
21104
|
-
const isUp =
|
|
21105
|
-
const isList =
|
|
21106
|
-
if (
|
|
21107
|
-
if (down &&
|
|
21108
|
-
if (
|
|
21385
|
+
if (!q2.token) return err(401, "a bearer token is required");
|
|
21386
|
+
const down = /^\/periscopes\/([^/]+)\/down$/.exec(q2.path);
|
|
21387
|
+
const isUp = q2.path === "/periscopes" && q2.method === "POST";
|
|
21388
|
+
const isList = q2.path === "/periscopes" && q2.method === "GET";
|
|
21389
|
+
if (q2.path === "/periscopes" && !isUp && !isList) return err(405, "method not allowed");
|
|
21390
|
+
if (down && q2.method !== "POST") return err(405, "method not allowed");
|
|
21391
|
+
if (q2.path !== "/periscopes" && !down) return err(404, "not found");
|
|
21109
21392
|
const member = async (workspaceId) => {
|
|
21110
|
-
const v = await h.verifyIn(
|
|
21393
|
+
const v = await h.verifyIn(q2.token, workspaceId);
|
|
21111
21394
|
if (v === "ok") return void 0;
|
|
21112
21395
|
if (v === "unavailable") return err(503, "could not verify the key right now");
|
|
21113
21396
|
return err(403, "this key does not belong to the workspace of this route");
|
|
21114
21397
|
};
|
|
21115
21398
|
if (isList) {
|
|
21116
|
-
const named =
|
|
21399
|
+
const named = q2.query.get("route") ?? void 0;
|
|
21117
21400
|
const candidates2 = named === void 0 ? h.routes.filter((r) => r.hostPeriscopes) : [pickRoute(h, named)];
|
|
21118
21401
|
const rows = [];
|
|
21119
21402
|
const hosting = [];
|
|
@@ -21145,7 +21428,7 @@ async function handlePeriscopes(h, q) {
|
|
|
21145
21428
|
}
|
|
21146
21429
|
return reply(200, { periscopes: rows, hosting });
|
|
21147
21430
|
}
|
|
21148
|
-
const b =
|
|
21431
|
+
const b = q2.body;
|
|
21149
21432
|
if (!b) return err(400, "body must be a small JSON object");
|
|
21150
21433
|
const routeName = plainText(b.route, "route", false);
|
|
21151
21434
|
if (isErr(routeName)) return err(400, routeName.error);
|
|
@@ -21166,7 +21449,7 @@ async function handlePeriscopes(h, q) {
|
|
|
21166
21449
|
`--${b.outcome}`,
|
|
21167
21450
|
...b.allowEmpty === true ? ["--allow-empty"] : [],
|
|
21168
21451
|
"--json"
|
|
21169
|
-
],
|
|
21452
|
+
], q2.wantsStream, h.mcp ? async (done) => {
|
|
21170
21453
|
await h.mcp.onDown(picked, done);
|
|
21171
21454
|
return done;
|
|
21172
21455
|
} : void 0);
|
|
@@ -21200,7 +21483,7 @@ async function handlePeriscopes(h, q) {
|
|
|
21200
21483
|
...h.mcp ? ["--serve-mcp"] : [],
|
|
21201
21484
|
"--no-open",
|
|
21202
21485
|
"--json"
|
|
21203
|
-
],
|
|
21486
|
+
], q2.wantsStream, h.mcp ? async (done) => ({ ...done, ...await h.mcp.onUp(picked, done) }) : void 0);
|
|
21204
21487
|
}
|
|
21205
21488
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
21206
21489
|
return new Promise((resolve7) => {
|
|
@@ -21325,7 +21608,7 @@ function candidates(live, rotated) {
|
|
|
21325
21608
|
const out = [live];
|
|
21326
21609
|
if (!rotated) return out;
|
|
21327
21610
|
try {
|
|
21328
|
-
for (const n of readdirSync18(
|
|
21611
|
+
for (const n of readdirSync18(dirname24(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname24(live), n));
|
|
21329
21612
|
} catch {
|
|
21330
21613
|
}
|
|
21331
21614
|
return out;
|
|
@@ -21475,17 +21758,17 @@ async function readJsonBody(req) {
|
|
|
21475
21758
|
return null;
|
|
21476
21759
|
}
|
|
21477
21760
|
}
|
|
21478
|
-
async function tailReply(path,
|
|
21479
|
-
const after =
|
|
21480
|
-
const tail3 =
|
|
21761
|
+
async function tailReply(path, q2, rotated, abort) {
|
|
21762
|
+
const after = q2.get("after") ?? void 0;
|
|
21763
|
+
const tail3 = q2.has("tail") ? Number(q2.get("tail")) : void 0;
|
|
21481
21764
|
if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err2(400, "tail must be an integer from 0 to 1000");
|
|
21482
|
-
const wait =
|
|
21765
|
+
const wait = q2.has("wait") ? Number(q2.get("wait")) : 0;
|
|
21483
21766
|
if (!(Number.isFinite(wait) && wait >= 0)) return err2(400, "wait must be a number of seconds");
|
|
21484
21767
|
if (after !== void 0 && !decodeCursor(after)) return err2(400, "after is not a cursor this API issued");
|
|
21485
21768
|
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
21486
21769
|
let r = readTail(path, { after, tail: tail3, rotated });
|
|
21487
21770
|
while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
|
|
21488
|
-
await waitForChange(
|
|
21771
|
+
await waitForChange(dirname24(path), deadline - Date.now(), abort);
|
|
21489
21772
|
r = readTail(path, { after: r.cursor, rotated });
|
|
21490
21773
|
}
|
|
21491
21774
|
return reply2(200, r);
|
|
@@ -21662,7 +21945,7 @@ function schedulerHandler(control, run3 = runDetached) {
|
|
|
21662
21945
|
}
|
|
21663
21946
|
function crewJson(args, timeoutMs = 6e4) {
|
|
21664
21947
|
return new Promise((resolve7) => {
|
|
21665
|
-
|
|
21948
|
+
execFile3(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
|
|
21666
21949
|
try {
|
|
21667
21950
|
const v = JSON.parse(String(stdout));
|
|
21668
21951
|
resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
@@ -21675,7 +21958,7 @@ function crewJson(args, timeoutMs = 6e4) {
|
|
|
21675
21958
|
|
|
21676
21959
|
// src/periscope-mcp.ts
|
|
21677
21960
|
import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
|
|
21678
|
-
import { basename as basename14, dirname as
|
|
21961
|
+
import { basename as basename14, dirname as dirname25, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
|
|
21679
21962
|
import { z } from "zod";
|
|
21680
21963
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21681
21964
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -21813,10 +22096,10 @@ async function realAncestor(p) {
|
|
|
21813
22096
|
}
|
|
21814
22097
|
if (isLink) {
|
|
21815
22098
|
if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
|
|
21816
|
-
cur = resolve5(
|
|
22099
|
+
cur = resolve5(dirname25(cur), await readlink(cur));
|
|
21817
22100
|
continue;
|
|
21818
22101
|
}
|
|
21819
|
-
const parent =
|
|
22102
|
+
const parent = dirname25(cur);
|
|
21820
22103
|
if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
|
|
21821
22104
|
tail3.push(basename14(cur));
|
|
21822
22105
|
cur = parent;
|
|
@@ -21839,7 +22122,7 @@ async function resolveInWorktree(root, requested) {
|
|
|
21839
22122
|
if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
|
|
21840
22123
|
return final;
|
|
21841
22124
|
}
|
|
21842
|
-
function
|
|
22125
|
+
function globToRegExp2(glob) {
|
|
21843
22126
|
let re = "";
|
|
21844
22127
|
for (let i = 0; i < glob.length; i++) {
|
|
21845
22128
|
const c = glob[i];
|
|
@@ -21913,7 +22196,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
21913
22196
|
inputSchema: { pattern: z.string() },
|
|
21914
22197
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
21915
22198
|
}, async ({ pattern }) => {
|
|
21916
|
-
const re =
|
|
22199
|
+
const re = globToRegExp2(pattern);
|
|
21917
22200
|
const listing = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
21918
22201
|
const hits = listing.split("\0").filter((f) => f && re.test(f)).slice(0, MAX_SEARCH_RESULTS);
|
|
21919
22202
|
return text(hits.length ? hits.join("\n") : "No matches found");
|
|
@@ -21927,7 +22210,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
21927
22210
|
if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
|
|
21928
22211
|
const abs = await inTree(path);
|
|
21929
22212
|
if (abs === await realpath(root)) return text("path must name a file", true);
|
|
21930
|
-
await mkdir(
|
|
22213
|
+
await mkdir(dirname25(abs), { recursive: true });
|
|
21931
22214
|
await inTree(path);
|
|
21932
22215
|
await writeFile(abs, content);
|
|
21933
22216
|
return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
|
|
@@ -21967,7 +22250,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
21967
22250
|
inputSchema: { path: z.string() },
|
|
21968
22251
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
21969
22252
|
}, async ({ path }) => {
|
|
21970
|
-
const parent = await inTree(
|
|
22253
|
+
const parent = await inTree(dirname25(path) === "" ? "." : dirname25(path));
|
|
21971
22254
|
const abs = join48(parent, basename14(path));
|
|
21972
22255
|
if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
|
|
21973
22256
|
const st = await lstat(abs).catch(() => void 0);
|
|
@@ -22089,7 +22372,7 @@ function findPeriscope(sidecars, segment) {
|
|
|
22089
22372
|
}
|
|
22090
22373
|
|
|
22091
22374
|
// src/cli.ts
|
|
22092
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
22375
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname26(process.execPath) : resolve6(dirname26(fileURLToPath(import.meta.url)), "..");
|
|
22093
22376
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
22094
22377
|
var CYCLE_SECONDS2 = 120;
|
|
22095
22378
|
var STALE_CYCLES = 3;
|
|
@@ -22138,12 +22421,12 @@ function terminalPrompter() {
|
|
|
22138
22421
|
process.stderr.write(`${line}
|
|
22139
22422
|
`);
|
|
22140
22423
|
},
|
|
22141
|
-
ask: async (
|
|
22142
|
-
const a = (await rl.question(fallback ? `${
|
|
22424
|
+
ask: async (q2, fallback) => {
|
|
22425
|
+
const a = (await rl.question(fallback ? `${q2} [${fallback}]: ` : `${q2}: `)).trim();
|
|
22143
22426
|
return a || fallback || "";
|
|
22144
22427
|
},
|
|
22145
|
-
confirm: async (
|
|
22146
|
-
const a = (await rl.question(`${
|
|
22428
|
+
confirm: async (q2, fallback) => {
|
|
22429
|
+
const a = (await rl.question(`${q2} [${fallback ? "Y/n" : "y/N"}] `)).trim();
|
|
22147
22430
|
return a ? /^y/i.test(a) : fallback;
|
|
22148
22431
|
}
|
|
22149
22432
|
};
|
|
@@ -22268,6 +22551,7 @@ function usage(problem, requested = false) {
|
|
|
22268
22551
|
crew shell-init [--shell zsh|bash|fish] print the periscope shell function (install: eval "$(crew shell-init)")
|
|
22269
22552
|
crew worktree [route] NNN cut (or refresh) this run's worktree for a ticket from the remote; prints its path
|
|
22270
22553
|
crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
|
|
22554
|
+
crew sandbox cache [--prune|--reset] [--json] list the sandbox cache volume's mirrors; --prune drops unserved repositories, --reset recreates the volume
|
|
22271
22555
|
crew sync [route] fast-forward the checkout and its worktrees from the remote
|
|
22272
22556
|
crew pause|resume [route] [R|release] pause everything, one role, or releases only (merge/deploy/release; passengers keeps running).
|
|
22273
22557
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
@@ -22584,6 +22868,31 @@ if (command === "passenger-sync-daemon") {
|
|
|
22584
22868
|
const out = refreshBaseBranch(r.dir, remote, base, true);
|
|
22585
22869
|
const unsafe = baseBranchUnsafe(out);
|
|
22586
22870
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
22871
|
+
}, sandboxCacheRepos = function(c, ship) {
|
|
22872
|
+
const out = [];
|
|
22873
|
+
for (const t of reposOf(c)) {
|
|
22874
|
+
const origin = originUrlOf(t.dir, "origin");
|
|
22875
|
+
const repo = origin ? parseRemote(origin) : null;
|
|
22876
|
+
if (!repo) continue;
|
|
22877
|
+
let token = null;
|
|
22878
|
+
try {
|
|
22879
|
+
token = readForgeToken(ship.forges, repo.host, ship.stateDir);
|
|
22880
|
+
} catch {
|
|
22881
|
+
}
|
|
22882
|
+
out.push({ ...repo, dir: t.dir, token, username: forgeFor(ship.forges, repo.host)?.entry.username });
|
|
22883
|
+
}
|
|
22884
|
+
return out;
|
|
22885
|
+
}, buildSandboxCachePass = function(c, ship, state2, emit2) {
|
|
22886
|
+
const settings = cacheSettingsOf(ship.sandbox);
|
|
22887
|
+
if (!settings) return void 0;
|
|
22888
|
+
return makeCachePass({
|
|
22889
|
+
cache: new SandboxCache(settings),
|
|
22890
|
+
volume: settings.volume,
|
|
22891
|
+
repos: () => sandboxCacheRepos(c, ship),
|
|
22892
|
+
lock: (dir) => acquireCheckoutLock(dir),
|
|
22893
|
+
fail: (key, message) => raiseShipAttention({ state: state2, emit: emit2, route: c, ship }, { kind: "sandbox_cache", key, message, since: (/* @__PURE__ */ new Date()).toISOString() }).then(() => void 0),
|
|
22894
|
+
ok: (key) => clearShipAttention(state2, key)
|
|
22895
|
+
});
|
|
22587
22896
|
}, sandboxTickDeps = function(emit2, c) {
|
|
22588
22897
|
return {
|
|
22589
22898
|
stateDir: cfg.ship.stateDir,
|
|
@@ -22597,7 +22906,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22597
22906
|
log: (m) => emit2.emit(m)
|
|
22598
22907
|
};
|
|
22599
22908
|
};
|
|
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;
|
|
22909
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth, sandboxCacheRepos2 = sandboxCacheRepos, buildSandboxCachePass2 = buildSandboxCachePass, sandboxTickDeps2 = sandboxTickDeps;
|
|
22601
22910
|
let cfg;
|
|
22602
22911
|
let firstRun;
|
|
22603
22912
|
let firstRunFromFlags = false;
|
|
@@ -22702,7 +23011,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22702
23011
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
22703
23012
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
22704
23013
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[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 === "
|
|
23014
|
+
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" || command === "sandbox" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
22706
23015
|
let route2;
|
|
22707
23016
|
try {
|
|
22708
23017
|
route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -23051,7 +23360,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
23051
23360
|
const out = /* @__PURE__ */ new Map();
|
|
23052
23361
|
for (const c of routes) {
|
|
23053
23362
|
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))
|
|
23363
|
+
for (const r of settled) if (r.config.sandbox && !out.has(r.name)) {
|
|
23364
|
+
let warm = {};
|
|
23365
|
+
try {
|
|
23366
|
+
warm = readWarmFiles(r.dir, `${r.config.branch.remote}/${r.config.branch.base}`, r.config.sandbox.warm ?? []);
|
|
23367
|
+
} catch {
|
|
23368
|
+
}
|
|
23369
|
+
out.set(r.name, { name: r.name, decl: r.config.sandbox, warm });
|
|
23370
|
+
}
|
|
23055
23371
|
}
|
|
23056
23372
|
return [...out.values()];
|
|
23057
23373
|
}
|
|
@@ -24225,6 +24541,7 @@ ${cu.tail.trimEnd()}
|
|
|
24225
24541
|
});
|
|
24226
24542
|
if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
|
|
24227
24543
|
const inFlight = /* @__PURE__ */ new Set();
|
|
24544
|
+
const sandboxCachePass = cfg.ship.runner === "sandbox" ? buildSandboxCachePass(route2, cfg.ship, state, emit) : void 0;
|
|
24228
24545
|
let sandboxBuilding = false;
|
|
24229
24546
|
const stopReason = await runDaemonLoop({
|
|
24230
24547
|
inFlight,
|
|
@@ -24233,6 +24550,7 @@ ${cu.tail.trimEnd()}
|
|
|
24233
24550
|
),
|
|
24234
24551
|
runPass: () => runOnePass({
|
|
24235
24552
|
inFlight,
|
|
24553
|
+
sandboxCache: sandboxCachePass,
|
|
24236
24554
|
route: route2,
|
|
24237
24555
|
ship: cfg.ship,
|
|
24238
24556
|
state,
|
|
@@ -24542,7 +24860,7 @@ ${cu.tail.trimEnd()}
|
|
|
24542
24860
|
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
24543
24861
|
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
24544
24862
|
const named2 = value("route") ? void 0 : positional[3];
|
|
24545
|
-
const dir = named2 ? repoDirOf(configText,
|
|
24863
|
+
const dir = named2 ? repoDirOf(configText, dirname26(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
24546
24864
|
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
24547
24865
|
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
24548
24866
|
runReposCheck(dir, {
|
|
@@ -24565,7 +24883,7 @@ ${cu.tail.trimEnd()}
|
|
|
24565
24883
|
if (sub === "hooks") {
|
|
24566
24884
|
try {
|
|
24567
24885
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
24568
|
-
const dir = repoDirOf(configText,
|
|
24886
|
+
const dir = repoDirOf(configText, dirname26(cfg.configFile), a.route, a.name);
|
|
24569
24887
|
if (!existsSync45(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
24570
24888
|
const files = { exists: (r) => existsSync45(resolvePath(dir, r)), list: (r) => {
|
|
24571
24889
|
try {
|
|
@@ -24732,7 +25050,7 @@ ${cu.tail.trimEnd()}
|
|
|
24732
25050
|
}
|
|
24733
25051
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
24734
25052
|
const text2 = configText;
|
|
24735
|
-
const base =
|
|
25053
|
+
const base = dirname26(cfg.configFile);
|
|
24736
25054
|
const isCheckout = (d) => existsSync45(`${d}/.git`);
|
|
24737
25055
|
try {
|
|
24738
25056
|
if (sub === "list") {
|
|
@@ -25408,7 +25726,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25408
25726
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
25409
25727
|
`);
|
|
25410
25728
|
} else {
|
|
25411
|
-
mkdirSync30(
|
|
25729
|
+
mkdirSync30(dirname26(resolvedPath), { recursive: true });
|
|
25412
25730
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
25413
25731
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
25414
25732
|
writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
@@ -25457,7 +25775,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25457
25775
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
25458
25776
|
`);
|
|
25459
25777
|
} else {
|
|
25460
|
-
mkdirSync30(
|
|
25778
|
+
mkdirSync30(dirname26(apiKeyFile), { recursive: true });
|
|
25461
25779
|
writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
25462
25780
|
`, { mode: 384 });
|
|
25463
25781
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
@@ -26028,7 +26346,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
26028
26346
|
if (subS === "images") {
|
|
26029
26347
|
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
26030
26348
|
const rows = declared.map((r) => {
|
|
26031
|
-
const plan = planSandbox(r.name, r.decl);
|
|
26349
|
+
const plan = planSandbox(r.name, r.decl, r.warm);
|
|
26032
26350
|
return { repo: r.name, wants: plan.tag, tags: stateS[r.name]?.tags ?? [], ...sandboxImageStatus(stateS, plan) };
|
|
26033
26351
|
});
|
|
26034
26352
|
if (jsonS) {
|
|
@@ -26062,7 +26380,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
26062
26380
|
for (const t of targets) {
|
|
26063
26381
|
await sandboxBuildTick([t], depsS, true);
|
|
26064
26382
|
const after = readSandboxState(cfg.ship.stateDir);
|
|
26065
|
-
const st = sandboxImageStatus(after, planSandbox(t.name, t.decl));
|
|
26383
|
+
const st = sandboxImageStatus(after, planSandbox(t.name, t.decl, t.warm));
|
|
26066
26384
|
if (st.status === "failed") failed = true;
|
|
26067
26385
|
process.stdout.write(`${t.name}: ${st.status} \u2014 ${st.detail}
|
|
26068
26386
|
`);
|
|
@@ -26781,7 +27099,7 @@ ${where.join("\n")}`);
|
|
|
26781
27099
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
26782
27100
|
const tracker = new Tracker(route2, cfg.ship);
|
|
26783
27101
|
const configText = readFileSync40(cfg.configFile, "utf8");
|
|
26784
|
-
const cfgBase =
|
|
27102
|
+
const cfgBase = dirname26(cfg.configFile);
|
|
26785
27103
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
26786
27104
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
26787
27105
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
@@ -26943,6 +27261,45 @@ ${where.join("\n")}`);
|
|
|
26943
27261
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
26944
27262
|
break;
|
|
26945
27263
|
}
|
|
27264
|
+
case "sandbox": {
|
|
27265
|
+
const settings = cacheSettingsOf(cfg.ship.sandbox);
|
|
27266
|
+
if (positional[1] !== "cache") {
|
|
27267
|
+
process.stderr.write("crew sandbox cache [--prune|--reset] [--json]\n");
|
|
27268
|
+
process.exit(2);
|
|
27269
|
+
}
|
|
27270
|
+
if (!settings) {
|
|
27271
|
+
process.stderr.write("crew sandbox cache: ship.sandbox.cacheVolume is not set, so there is no cache volume\n");
|
|
27272
|
+
process.exit(2);
|
|
27273
|
+
}
|
|
27274
|
+
const cache = new SandboxCache(settings);
|
|
27275
|
+
const served = cfg.routes.flatMap((r) => sandboxCacheRepos(r, cfg.ship));
|
|
27276
|
+
try {
|
|
27277
|
+
if (flag("reset")) {
|
|
27278
|
+
await cache.reset();
|
|
27279
|
+
process.stdout.write(`recreated volume ${settings.volume}; the next daemon pass refreshes every mirror
|
|
27280
|
+
`);
|
|
27281
|
+
break;
|
|
27282
|
+
}
|
|
27283
|
+
const dropped = flag("prune") ? await cache.prune(served) : [];
|
|
27284
|
+
const mirrors = await cache.list();
|
|
27285
|
+
if (flag("json")) {
|
|
27286
|
+
process.stdout.write(`${JSON.stringify({ volume: settings.volume, mirrors, dropped }, null, 2)}
|
|
27287
|
+
`);
|
|
27288
|
+
} else {
|
|
27289
|
+
for (const d of dropped) process.stdout.write(`dropped ${d.host}/${d.path}
|
|
27290
|
+
`);
|
|
27291
|
+
if (mirrors.length === 0) process.stdout.write(`volume ${settings.volume}: no mirrors yet
|
|
27292
|
+
`);
|
|
27293
|
+
for (const m of mirrors) process.stdout.write(`${m.repo.host}/${m.repo.path} last refresh ${m.lastRefresh ?? "never"}
|
|
27294
|
+
`);
|
|
27295
|
+
}
|
|
27296
|
+
} catch (e) {
|
|
27297
|
+
process.stderr.write(`crew sandbox cache: ${e instanceof Error ? e.message : String(e)}
|
|
27298
|
+
`);
|
|
27299
|
+
process.exitCode = 1;
|
|
27300
|
+
}
|
|
27301
|
+
break;
|
|
27302
|
+
}
|
|
26946
27303
|
case "whoami": {
|
|
26947
27304
|
const only = value("route") ?? positional[1];
|
|
26948
27305
|
const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
|
|
@@ -27761,7 +28118,7 @@ ${foreign.join("\n")}
|
|
|
27761
28118
|
const declaredS = await sandboxReposOf(cfg.routes.filter((r) => r.enabled));
|
|
27762
28119
|
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
27763
28120
|
for (const r of declaredS) {
|
|
27764
|
-
const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl));
|
|
28121
|
+
const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl, r.warm));
|
|
27765
28122
|
process.stdout.write(`sandbox image: ${r.name}: ${st.status.toUpperCase()} \u2014 ${st.detail}
|
|
27766
28123
|
`);
|
|
27767
28124
|
}
|
|
@@ -27824,4 +28181,6 @@ var endpointReconcileDeps2;
|
|
|
27824
28181
|
var releaseRefused2;
|
|
27825
28182
|
var doctorMachineId2;
|
|
27826
28183
|
var baseHealth2;
|
|
28184
|
+
var sandboxCacheRepos2;
|
|
28185
|
+
var buildSandboxCachePass2;
|
|
27827
28186
|
var sandboxTickDeps2;
|