cubefarm 0.3.8 → 0.3.10
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/README.md +13 -3
- package/dist/assets/ToyWorld-CgbZTjI5.js +2 -0
- package/dist/assets/{index-B7sInPPW.js → index-DxPWlxj7.js} +68 -68
- package/dist/assets/index-qBB5_EtY.css +1 -0
- package/dist/assets/{rapier-sDtDRX5B.js → rapier-BkzkQl77.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +1064 -340
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/assets/ToyWorld-DAEljc3S.js +0 -2
- package/dist/assets/index-CIr98Zaa.css +0 -1
package/dist-server/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// server/index.ts
|
|
2
2
|
import http2 from "node:http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path16 from "node:path";
|
|
4
|
+
import fs14 from "node:fs";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
|
+
import { z as z3 } from "zod";
|
|
7
8
|
|
|
8
9
|
// server/config.ts
|
|
9
10
|
import fs from "node:fs";
|
|
@@ -146,11 +147,11 @@ async function ensureLabel(fullName, name) {
|
|
|
146
147
|
await gh(["label", "create", name, "-R", fullName, "--color", "c77dff", "--description", "cubefarm: routed to this specialty", "--force"]);
|
|
147
148
|
labelsMade.add(key);
|
|
148
149
|
}
|
|
149
|
-
async function createIssue(fullName, title,
|
|
150
|
+
async function createIssue(fullName, title, body2, labels = []) {
|
|
150
151
|
for (const l of labels) await ensureLabel(fullName, l);
|
|
151
152
|
const args = ["issue", "create", "-R", fullName, "--title", title, "--body-file", "-"];
|
|
152
153
|
for (const l of labels) args.push("--label", l);
|
|
153
|
-
const out = await gh(args, { input:
|
|
154
|
+
const out = await gh(args, { input: body2 || " " });
|
|
154
155
|
const match = out.match(/\/issues\/(\d+)/);
|
|
155
156
|
if (!match) throw new Error(`Could not read the new issue number from gh output: ${out}`);
|
|
156
157
|
return Number(match[1]);
|
|
@@ -194,8 +195,10 @@ async function prForBranch(fullName, branch) {
|
|
|
194
195
|
const list = await ghJson(["pr", "list", "-R", fullName, "--head", branch, "--state", "all", "--json", "number,url", "--limit", "1"]);
|
|
195
196
|
return list[0] ?? null;
|
|
196
197
|
}
|
|
198
|
+
var uniqueNames = (checks) => [...new Set(checks.map(checkName))];
|
|
197
199
|
async function prDetails(fullName, number) {
|
|
198
|
-
const raw = await ghJson(["pr", "view", String(number), "-R", fullName, "--json", "number,title,body,url,headRefName,headRefOid,isCrossRepository,closingIssuesReferences,state,mergeable,mergeStateStatus"]);
|
|
200
|
+
const raw = await ghJson(["pr", "view", String(number), "-R", fullName, "--json", "number,title,body,url,headRefName,headRefOid,isCrossRepository,closingIssuesReferences,state,mergeable,mergeStateStatus,statusCheckRollup"]);
|
|
201
|
+
const rollup = raw.statusCheckRollup ?? [];
|
|
199
202
|
return {
|
|
200
203
|
number: raw.number,
|
|
201
204
|
title: raw.title,
|
|
@@ -207,20 +210,24 @@ async function prDetails(fullName, number) {
|
|
|
207
210
|
closesIssues: (raw.closingIssuesReferences ?? []).map((r) => r.number),
|
|
208
211
|
state: raw.state,
|
|
209
212
|
mergeable: raw.mergeable,
|
|
210
|
-
mergeState: raw.mergeStateStatus || "UNKNOWN"
|
|
213
|
+
mergeState: raw.mergeStateStatus || "UNKNOWN",
|
|
214
|
+
checks: checksOf(rollup),
|
|
215
|
+
checkNames: uniqueNames(rollup),
|
|
216
|
+
failedChecks: uniqueNames(rollup.filter(failed)),
|
|
217
|
+
pendingChecks: uniqueNames(rollup.filter(pending))
|
|
211
218
|
};
|
|
212
219
|
}
|
|
213
220
|
async function issueDetails(fullName, number) {
|
|
214
221
|
const raw = await ghJson(["issue", "view", String(number), "-R", fullName, "--json", "title,body"]);
|
|
215
222
|
return { title: raw.title, body: raw.body ?? "" };
|
|
216
223
|
}
|
|
217
|
-
async function commentPull(fullName, number,
|
|
218
|
-
const out = await gh(["pr", "comment", String(number), "-R", fullName, "--body-file", "-"], { input:
|
|
224
|
+
async function commentPull(fullName, number, body2) {
|
|
225
|
+
const out = await gh(["pr", "comment", String(number), "-R", fullName, "--body-file", "-"], { input: body2 });
|
|
219
226
|
return out.match(/https:\/\/github\.com\/\S+/)?.[0] ?? "";
|
|
220
227
|
}
|
|
221
228
|
var EVIDENCE_BRANCH = "swarm-qa-evidence";
|
|
222
229
|
var evidenceReady = /* @__PURE__ */ new Set();
|
|
223
|
-
var ghApiJson = (method, endpoint,
|
|
230
|
+
var ghApiJson = (method, endpoint, body2) => gh(["api", "-X", method, endpoint, "-H", "Content-Type: application/json", "--input", "-"], { input: JSON.stringify(body2), timeoutMs: 12e4 }).then(
|
|
224
231
|
(out) => out ? JSON.parse(out) : null
|
|
225
232
|
);
|
|
226
233
|
async function ensureEvidenceBranch(fullName) {
|
|
@@ -457,7 +464,32 @@ async function createProject(root, name, opts) {
|
|
|
457
464
|
async function removeDir(dir) {
|
|
458
465
|
await fs2.rm(dir, { recursive: true, force: true, maxRetries: 8, retryDelay: 250 });
|
|
459
466
|
}
|
|
460
|
-
|
|
467
|
+
var STALE_LOCK_MS = 10 * 6e4;
|
|
468
|
+
async function staleIndexLock(wt) {
|
|
469
|
+
const ref = (await fs2.readFile(path2.join(wt, ".git"), "utf8").catch(() => "")).match(/gitdir:\s*(.+)/)?.[1]?.trim();
|
|
470
|
+
if (!ref) return null;
|
|
471
|
+
const lock = path2.join(path2.resolve(wt, ref), "index.lock");
|
|
472
|
+
const stat = await fs2.stat(lock).catch(() => null);
|
|
473
|
+
return stat && Date.now() - stat.mtimeMs > STALE_LOCK_MS ? lock : null;
|
|
474
|
+
}
|
|
475
|
+
async function reuseDesk(wt, branch, ref) {
|
|
476
|
+
await git(["checkout", "--force", "-B", branch, ref], { cwd: wt });
|
|
477
|
+
await git(["reset", "--hard", ref], { cwd: wt });
|
|
478
|
+
await git(["clean", "-fd"], { cwd: wt }).catch(() => void 0);
|
|
479
|
+
}
|
|
480
|
+
async function clearDeskFolder(wt) {
|
|
481
|
+
try {
|
|
482
|
+
await removeDir(wt);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
const left = await fs2.readdir(wt).catch(() => null);
|
|
485
|
+
if (left?.length !== 0) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`Could not clear the desk folder ${wt}: ${err.message}. A program started by the previous task is probably still running there. Close it and try again.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function prepareDesk(fullName, base, agentSlug, branch, note) {
|
|
461
493
|
return withRepoLock(fullName, async () => {
|
|
462
494
|
const main = mainDir(fullName);
|
|
463
495
|
await git(["fetch", "origin", "--prune"], { cwd: main, timeoutMs: 18e4 });
|
|
@@ -476,22 +508,25 @@ function prepareDesk(fullName, base, agentSlug, branch) {
|
|
|
476
508
|
const wt = deskDir(fullName, agentSlug);
|
|
477
509
|
if (await exists(path2.join(wt, ".git"))) {
|
|
478
510
|
try {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
511
|
+
try {
|
|
512
|
+
await reuseDesk(wt, branch, ref);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
const lock = /index\.lock/.test(err.message) ? await staleIndexLock(wt) : null;
|
|
515
|
+
if (!lock) throw err;
|
|
516
|
+
await fs2.rm(lock, { force: true, maxRetries: 3 });
|
|
517
|
+
note?.(`Removed a stale git lock (${lock}) left in the desk.`);
|
|
518
|
+
await reuseDesk(wt, branch, ref);
|
|
519
|
+
}
|
|
482
520
|
return wt;
|
|
483
|
-
} catch {
|
|
521
|
+
} catch (err) {
|
|
522
|
+
const why = `Couldn't reuse the desk in place, so it's rebuilt: ${err.message}`;
|
|
523
|
+
console.warn(`${wt}: ${why}`);
|
|
524
|
+
note?.(why);
|
|
484
525
|
}
|
|
485
526
|
}
|
|
486
527
|
if (await exists(wt)) {
|
|
487
528
|
await git(["worktree", "remove", "--force", wt], { cwd: main }).catch(() => void 0);
|
|
488
|
-
|
|
489
|
-
await removeDir(wt);
|
|
490
|
-
} catch (err) {
|
|
491
|
-
throw new Error(
|
|
492
|
-
`Could not clear the desk folder ${wt}: ${err.message}. A program started by the previous task is probably still running there. Close it and try again.`
|
|
493
|
-
);
|
|
494
|
-
}
|
|
529
|
+
await clearDeskFolder(wt);
|
|
495
530
|
}
|
|
496
531
|
await git(["worktree", "prune"], { cwd: main });
|
|
497
532
|
await fs2.mkdir(path2.dirname(wt), { recursive: true });
|
|
@@ -785,14 +820,14 @@ function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
|
785
820
|
}
|
|
786
821
|
return found2;
|
|
787
822
|
}
|
|
788
|
-
var clip = (
|
|
823
|
+
var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
|
|
789
824
|
function rel(cwd, p) {
|
|
790
|
-
const
|
|
791
|
-
if (!
|
|
792
|
-
const r = path4.relative(cwd, path4.resolve(cwd,
|
|
793
|
-
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") :
|
|
825
|
+
const s2 = String(p ?? "");
|
|
826
|
+
if (!s2) return "";
|
|
827
|
+
const r = path4.relative(cwd, path4.resolve(cwd, s2));
|
|
828
|
+
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s2;
|
|
794
829
|
}
|
|
795
|
-
var escapeRe = (
|
|
830
|
+
var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
796
831
|
function tidyPaths(cwd, text) {
|
|
797
832
|
let out = text;
|
|
798
833
|
for (const v of /* @__PURE__ */ new Set([cwd, cwd.replaceAll("\\", "/")])) {
|
|
@@ -972,7 +1007,7 @@ function startSession(opts, callbacks) {
|
|
|
972
1007
|
case "system": {
|
|
973
1008
|
if (msg.subtype === "init") {
|
|
974
1009
|
cb.sessionId(msg.session_id);
|
|
975
|
-
const mcp = msg.mcp_servers.map((
|
|
1010
|
+
const mcp = msg.mcp_servers.map((s2) => `${s2.name}:${s2.status}`).join(", ");
|
|
976
1011
|
cb.log([
|
|
977
1012
|
{ kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort` },
|
|
978
1013
|
{ kind: "system", text: ` cwd ${msg.cwd}${mcp ? ` \xB7 mcp ${mcp}` : ""}` }
|
|
@@ -1189,7 +1224,7 @@ async function detectClis() {
|
|
|
1189
1224
|
var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
|
|
1190
1225
|
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
|
|
1191
1226
|
function codexHookCommand(node, script, win = WIN) {
|
|
1192
|
-
return win ? `node "${script}"` : [node, script].map((
|
|
1227
|
+
return win ? `node "${script}"` : [node, script].map((s2) => `'${s2.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
1193
1228
|
}
|
|
1194
1229
|
function launchArgs(id, ctx) {
|
|
1195
1230
|
switch (id) {
|
|
@@ -1219,7 +1254,7 @@ function launchArgs(id, ctx) {
|
|
|
1219
1254
|
return { args, env: { DISABLE_AUTOUPDATER: "1" } };
|
|
1220
1255
|
}
|
|
1221
1256
|
case "codex": {
|
|
1222
|
-
const toml = (
|
|
1257
|
+
const toml = (s2) => JSON.stringify(s2);
|
|
1223
1258
|
const args = [
|
|
1224
1259
|
"--no-alt-screen",
|
|
1225
1260
|
"-c",
|
|
@@ -1623,11 +1658,11 @@ function setOfficeUrl(url) {
|
|
|
1623
1658
|
officeUrl = url;
|
|
1624
1659
|
}
|
|
1625
1660
|
var routes = /* @__PURE__ */ new Map();
|
|
1626
|
-
function handleHook(token,
|
|
1661
|
+
function handleHook(token, body2) {
|
|
1627
1662
|
const route2 = routes.get(token);
|
|
1628
|
-
if (!route2 || !
|
|
1663
|
+
if (!route2 || !body2 || typeof body2 !== "object" || Array.isArray(body2)) return {};
|
|
1629
1664
|
try {
|
|
1630
|
-
return route2.hook(
|
|
1665
|
+
return route2.hook(body2);
|
|
1631
1666
|
} catch (err) {
|
|
1632
1667
|
console.warn("hook failed", err);
|
|
1633
1668
|
return {};
|
|
@@ -1674,7 +1709,7 @@ function toolOutput(response) {
|
|
|
1674
1709
|
if (typeof response === "string") return { text: response, images };
|
|
1675
1710
|
const r = response;
|
|
1676
1711
|
if (r && typeof r === "object") {
|
|
1677
|
-
if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((
|
|
1712
|
+
if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((s2) => typeof s2 === "string" && s2).join("\n"), images };
|
|
1678
1713
|
if (typeof r.file?.content === "string") return { text: r.file.content, images };
|
|
1679
1714
|
if (Array.isArray(r.filenames)) return { text: r.filenames.join("\n"), images };
|
|
1680
1715
|
}
|
|
@@ -1702,12 +1737,13 @@ function endLive(live) {
|
|
|
1702
1737
|
routes.delete(live.token);
|
|
1703
1738
|
live.term.releaseIdle = null;
|
|
1704
1739
|
live.term.bind(null);
|
|
1705
|
-
const gone = live.proc ? exitOf(live.proc) :
|
|
1740
|
+
const gone = live.proc ? exitOf(live.proc) : Promise.resolve();
|
|
1706
1741
|
live.proc?.kill();
|
|
1707
1742
|
live.proc = null;
|
|
1708
1743
|
live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
|
|
1709
1744
|
fs7.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
|
|
1710
|
-
|
|
1745
|
+
if (live.cli === "codex" && live.resumeId) void codexThread("archive", live.resumeId, gone);
|
|
1746
|
+
return gone;
|
|
1711
1747
|
}
|
|
1712
1748
|
var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
|
|
1713
1749
|
var remember = (l) => l.proc?.setMeta(metaOf(l));
|
|
@@ -2156,19 +2192,137 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
2156
2192
|
return handle;
|
|
2157
2193
|
}
|
|
2158
2194
|
|
|
2159
|
-
// server/
|
|
2195
|
+
// server/deps.ts
|
|
2160
2196
|
import { spawn as spawn2 } from "node:child_process";
|
|
2161
2197
|
import crypto4 from "node:crypto";
|
|
2162
2198
|
import fs8 from "node:fs/promises";
|
|
2163
|
-
import net2 from "node:net";
|
|
2164
2199
|
import path9 from "node:path";
|
|
2200
|
+
var INSTALL_MARKER = ".cubefarm-preview-install";
|
|
2201
|
+
var INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
2202
|
+
var MAX_DESK_INSTALLS = 2;
|
|
2203
|
+
var depsHash = (pkg, lock) => crypto4.createHash("sha256").update(pkg).update("\0").update(lock ?? "").digest("hex");
|
|
2204
|
+
async function readDeps(dir) {
|
|
2205
|
+
const read = (file) => fs8.readFile(path9.join(dir, file), "utf8").catch(() => null);
|
|
2206
|
+
const [pkg, lock, marker, nodeModules] = await Promise.all([
|
|
2207
|
+
read("package.json"),
|
|
2208
|
+
read("package-lock.json"),
|
|
2209
|
+
read(path9.join("node_modules", INSTALL_MARKER)),
|
|
2210
|
+
fs8.stat(path9.join(dir, "node_modules")).then((s2) => s2.isDirectory()).catch(() => false)
|
|
2211
|
+
]);
|
|
2212
|
+
return { pkg, lock, hash: depsHash(pkg ?? "", lock), marker: marker ?? "", nodeModules };
|
|
2213
|
+
}
|
|
2214
|
+
var depsCurrent = (s2) => s2.nodeModules && s2.marker === s2.hash;
|
|
2215
|
+
var writeMarker = (dir, hash) => fs8.writeFile(path9.join(dir, "node_modules", INSTALL_MARKER), hash).catch(() => void 0);
|
|
2216
|
+
function deskDepsPlan(s2) {
|
|
2217
|
+
if (s2.pkg === null) return "none";
|
|
2218
|
+
if (s2.lock === null) return "agent";
|
|
2219
|
+
return depsCurrent(s2) ? "skip" : "install";
|
|
2220
|
+
}
|
|
2221
|
+
function depsPromptLine(outcome) {
|
|
2222
|
+
if (outcome === "installed" || outcome === "skipped") return "Dependencies are already installed for this checkout; run npm install only if you change package.json.";
|
|
2223
|
+
if (outcome === "failed") return "The office couldn't install this checkout's dependencies: run npm install yourself first.";
|
|
2224
|
+
return "";
|
|
2225
|
+
}
|
|
2226
|
+
function createLimiter(max) {
|
|
2227
|
+
let active = 0;
|
|
2228
|
+
const waiting = [];
|
|
2229
|
+
return async function limit(fn) {
|
|
2230
|
+
if (active >= max) await new Promise((r) => waiting.push(r));
|
|
2231
|
+
else active++;
|
|
2232
|
+
try {
|
|
2233
|
+
return await fn();
|
|
2234
|
+
} finally {
|
|
2235
|
+
const next = waiting.shift();
|
|
2236
|
+
if (next) next();
|
|
2237
|
+
else active--;
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
var deskInstalls = createLimiter(MAX_DESK_INSTALLS);
|
|
2242
|
+
function killTree2(child) {
|
|
2243
|
+
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
2244
|
+
if (process.platform === "win32") {
|
|
2245
|
+
return new Promise((resolve) => {
|
|
2246
|
+
const tk = spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
2247
|
+
tk.once("error", () => resolve());
|
|
2248
|
+
tk.once("close", () => resolve());
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
try {
|
|
2252
|
+
process.kill(-child.pid, "SIGKILL");
|
|
2253
|
+
} catch {
|
|
2254
|
+
child.kill("SIGKILL");
|
|
2255
|
+
}
|
|
2256
|
+
return Promise.resolve();
|
|
2257
|
+
}
|
|
2258
|
+
function npmCi(dir, timeoutMs = INSTALL_TIMEOUT_MS) {
|
|
2259
|
+
const env = {};
|
|
2260
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
2261
|
+
if (v !== void 0 && !/^(ANTHROPIC_|CLAUDE|SWARM_)/i.test(k)) env[k] = v;
|
|
2262
|
+
}
|
|
2263
|
+
env.NO_COLOR = "1";
|
|
2264
|
+
const win = process.platform === "win32";
|
|
2265
|
+
const child = win ? spawn2("cmd.exe", ["/d", "/s", "/c", "npm ci --no-audit --no-fund"], { cwd: dir, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }) : spawn2("npm", ["ci", "--no-audit", "--no-fund"], { cwd: dir, env, windowsHide: true, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
2266
|
+
let tail = "";
|
|
2267
|
+
const keep = (buf) => tail = (tail + buf.toString()).slice(-4e3);
|
|
2268
|
+
child.stdout?.on("data", keep);
|
|
2269
|
+
child.stderr?.on("data", keep);
|
|
2270
|
+
return new Promise((resolve, reject) => {
|
|
2271
|
+
const timer = setTimeout(() => {
|
|
2272
|
+
void killTree2(child);
|
|
2273
|
+
reject(new Error(`npm ci took longer than ${Math.round(timeoutMs / 6e4)} minutes`));
|
|
2274
|
+
}, timeoutMs);
|
|
2275
|
+
child.once("error", (err) => {
|
|
2276
|
+
clearTimeout(timer);
|
|
2277
|
+
reject(err);
|
|
2278
|
+
});
|
|
2279
|
+
child.once("close", (code) => {
|
|
2280
|
+
clearTimeout(timer);
|
|
2281
|
+
if (code === 0) return resolve();
|
|
2282
|
+
const last = tail.split(/\r?\n/).filter((l) => l.trim()).slice(-3).join(" \xB7 ");
|
|
2283
|
+
reject(new Error(`npm ci failed (code ${code})${last ? `: ${last.slice(0, 300)}` : ""}`));
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
async function installDesk(dir, cb, install = npmCi) {
|
|
2288
|
+
const { log } = cb;
|
|
2289
|
+
try {
|
|
2290
|
+
const state = await readDeps(dir);
|
|
2291
|
+
const plan = deskDepsPlan(state);
|
|
2292
|
+
if (plan === "none") return "none";
|
|
2293
|
+
if (plan === "agent") {
|
|
2294
|
+
log(["No package-lock.json; leaving the dependencies to the agent."]);
|
|
2295
|
+
return "agent";
|
|
2296
|
+
}
|
|
2297
|
+
if (plan === "skip") {
|
|
2298
|
+
log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2299
|
+
return "skipped";
|
|
2300
|
+
}
|
|
2301
|
+
log(["Installing dependencies\u2026"]);
|
|
2302
|
+
cb.installing();
|
|
2303
|
+
await deskInstalls(async () => {
|
|
2304
|
+
log(["$ npm ci"]);
|
|
2305
|
+
await install(dir);
|
|
2306
|
+
});
|
|
2307
|
+
await writeMarker(dir, state.hash);
|
|
2308
|
+
log(["Dependencies installed."]);
|
|
2309
|
+
return "installed";
|
|
2310
|
+
} catch (err) {
|
|
2311
|
+
log([`\u26A0 Could not install the dependencies (${err.message.split(/\r?\n/)[0].slice(0, 300)}); the agent will run npm install itself.`]);
|
|
2312
|
+
return "failed";
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// server/previewRunner.ts
|
|
2317
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2318
|
+
import fs9 from "node:fs/promises";
|
|
2319
|
+
import net2 from "node:net";
|
|
2320
|
+
import path10 from "node:path";
|
|
2165
2321
|
var PREVIEW_SLUG = "preview";
|
|
2166
2322
|
var PREVIEW_BRANCH = "swarm-preview";
|
|
2167
2323
|
var PREVIEW_TMP = ".preview-tmp";
|
|
2168
|
-
var INSTALL_MARKER = ".cubefarm-preview-install";
|
|
2169
2324
|
var START_TIMEOUT_MS = 3 * 6e4;
|
|
2170
|
-
var
|
|
2171
|
-
var fillPlaceholders = (s, port, tmp) => s.replaceAll("{port}", String(port)).replaceAll("{tmp}", tmp);
|
|
2325
|
+
var fillPlaceholders = (s2, port, tmp) => s2.replaceAll("{port}", String(port)).replaceAll("{tmp}", tmp);
|
|
2172
2326
|
function previewEnv(extra, port, tmp) {
|
|
2173
2327
|
const env = {};
|
|
2174
2328
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -2197,35 +2351,19 @@ async function portOpen(port) {
|
|
|
2197
2351
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2198
2352
|
async function readJson(file) {
|
|
2199
2353
|
try {
|
|
2200
|
-
return JSON.parse(await
|
|
2354
|
+
return JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2201
2355
|
} catch {
|
|
2202
2356
|
return null;
|
|
2203
2357
|
}
|
|
2204
2358
|
}
|
|
2205
2359
|
function defaultCommand(pkg) {
|
|
2206
2360
|
const scripts = pkg?.scripts ?? {};
|
|
2207
|
-
const name = ["dev", "start", "preview"].find((
|
|
2361
|
+
const name = ["dev", "start", "preview"].find((s2) => typeof scripts[s2] === "string" && scripts[s2].trim());
|
|
2208
2362
|
if (!name) return null;
|
|
2209
2363
|
const script = scripts[name];
|
|
2210
2364
|
const vite = /^\s*vite(\s|$)/.test(script) && !/--port\b/.test(script);
|
|
2211
2365
|
return `npm run ${name}${vite ? " -- --port {port} --strictPort" : ""}`;
|
|
2212
2366
|
}
|
|
2213
|
-
function killTree2(child) {
|
|
2214
|
-
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
2215
|
-
if (process.platform === "win32") {
|
|
2216
|
-
return new Promise((resolve) => {
|
|
2217
|
-
const tk = spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
2218
|
-
tk.once("error", () => resolve());
|
|
2219
|
-
tk.once("close", () => resolve());
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
try {
|
|
2223
|
-
process.kill(-child.pid, "SIGKILL");
|
|
2224
|
-
} catch {
|
|
2225
|
-
child.kill("SIGKILL");
|
|
2226
|
-
}
|
|
2227
|
-
return Promise.resolve();
|
|
2228
|
-
}
|
|
2229
2367
|
var Stopped = class extends Error {
|
|
2230
2368
|
};
|
|
2231
2369
|
var RealPreview = class {
|
|
@@ -2258,9 +2396,9 @@ var RealPreview = class {
|
|
|
2258
2396
|
this.check();
|
|
2259
2397
|
await ensureLocalExcludes(job.fullName).catch(() => void 0);
|
|
2260
2398
|
cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
|
|
2261
|
-
const tmp =
|
|
2262
|
-
await
|
|
2263
|
-
const pkg = await readJson(
|
|
2399
|
+
const tmp = path10.join(wt, PREVIEW_TMP);
|
|
2400
|
+
await fs9.mkdir(tmp, { recursive: true });
|
|
2401
|
+
const pkg = await readJson(path10.join(wt, "package.json"));
|
|
2264
2402
|
const command = job.command ?? defaultCommand(pkg);
|
|
2265
2403
|
if (!command) {
|
|
2266
2404
|
this.stopped = true;
|
|
@@ -2299,15 +2437,12 @@ var RealPreview = class {
|
|
|
2299
2437
|
}
|
|
2300
2438
|
/** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
|
|
2301
2439
|
async install(wt, env) {
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
const hash = crypto4.createHash("sha256").update(await fs8.readFile(path9.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
|
|
2305
|
-
const marker = path9.join(wt, "node_modules", INSTALL_MARKER);
|
|
2306
|
-
if (await fs8.readFile(marker, "utf8").catch(() => "") === hash) {
|
|
2440
|
+
const deps = await readDeps(wt);
|
|
2441
|
+
if (depsCurrent(deps)) {
|
|
2307
2442
|
this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2308
2443
|
return;
|
|
2309
2444
|
}
|
|
2310
|
-
const cmd = lock !== null ? "npm ci" : "npm install";
|
|
2445
|
+
const cmd = deps.lock !== null ? "npm ci" : "npm install";
|
|
2311
2446
|
this.cb.log([`$ ${cmd}`]);
|
|
2312
2447
|
const code = await Promise.race([this.spawn(cmd, wt, env), sleep2(INSTALL_TIMEOUT_MS).then(() => "timeout")]);
|
|
2313
2448
|
this.check();
|
|
@@ -2317,12 +2452,12 @@ var RealPreview = class {
|
|
|
2317
2452
|
}
|
|
2318
2453
|
if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
|
|
2319
2454
|
this.child = null;
|
|
2320
|
-
await
|
|
2455
|
+
await writeMarker(wt, deps.hash);
|
|
2321
2456
|
}
|
|
2322
2457
|
/** Run a command line through the platform shell; resolves with its exit code. */
|
|
2323
2458
|
spawn(cmd, cwd, env) {
|
|
2324
2459
|
this.check();
|
|
2325
|
-
const child =
|
|
2460
|
+
const child = spawn3(cmd, { cwd, env, shell: true, windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
2326
2461
|
this.child = child;
|
|
2327
2462
|
const onData = (buf) => {
|
|
2328
2463
|
const text = this.partial + buf.toString().replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
@@ -2348,29 +2483,29 @@ var RealPreview = class {
|
|
|
2348
2483
|
};
|
|
2349
2484
|
var realPreviews = {
|
|
2350
2485
|
start: (job, cb) => new RealPreview(job, cb),
|
|
2351
|
-
hasDefault: async (fullName) =>
|
|
2486
|
+
hasDefault: async (fullName) => fs9.access(path10.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
2352
2487
|
};
|
|
2353
2488
|
|
|
2354
2489
|
// server/officeUpdate.ts
|
|
2355
|
-
import
|
|
2356
|
-
import
|
|
2490
|
+
import fs10 from "node:fs/promises";
|
|
2491
|
+
import path11 from "node:path";
|
|
2357
2492
|
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
2358
2493
|
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
2359
2494
|
function underLauncher(proc = process) {
|
|
2360
2495
|
return typeof proc.send === "function" && proc.env.SWARM_LAUNCHER === "1";
|
|
2361
2496
|
}
|
|
2362
|
-
function drainDecision(
|
|
2497
|
+
function drainDecision(s2) {
|
|
2363
2498
|
const idle = (state) => ({ state, drainingSince: null, send: false, timedOut: false });
|
|
2364
|
-
if (
|
|
2365
|
-
if (
|
|
2366
|
-
if (!
|
|
2367
|
-
const postponed =
|
|
2368
|
-
const failedHold =
|
|
2369
|
-
const due =
|
|
2370
|
-
if (!due) return idle(
|
|
2371
|
-
const since =
|
|
2372
|
-
const timedOut =
|
|
2373
|
-
return { state: "draining", drainingSince: since, send:
|
|
2499
|
+
if (s2.sent) return { state: "updating", drainingSince: s2.drainingSince, send: false, timedOut: false };
|
|
2500
|
+
if (s2.behind <= 0) return idle("none");
|
|
2501
|
+
if (!s2.launcher) return idle(s2.failed ? "failed" : "available");
|
|
2502
|
+
const postponed = s2.postponedUntil !== null && s2.now < s2.postponedUntil && s2.behind <= s2.postponedBehind;
|
|
2503
|
+
const failedHold = s2.failed !== null && s2.behind <= (s2.failedBehind ?? Infinity);
|
|
2504
|
+
const due = s2.requested || s2.autoUpdate && !postponed && !failedHold;
|
|
2505
|
+
if (!due) return idle(s2.failed ? "failed" : s2.autoUpdate && postponed ? "waiting" : "available");
|
|
2506
|
+
const since = s2.drainingSince ?? s2.now;
|
|
2507
|
+
const timedOut = s2.running > 0 && s2.now - since >= DRAIN_TIMEOUT_MS;
|
|
2508
|
+
return { state: "draining", drainingSince: since, send: s2.running === 0 || timedOut, timedOut };
|
|
2374
2509
|
}
|
|
2375
2510
|
function parseLastUpdate(raw) {
|
|
2376
2511
|
try {
|
|
@@ -2390,10 +2525,10 @@ function parseLastUpdate(raw) {
|
|
|
2390
2525
|
}
|
|
2391
2526
|
}
|
|
2392
2527
|
async function takeLastUpdate(home) {
|
|
2393
|
-
const file =
|
|
2394
|
-
const raw = await
|
|
2528
|
+
const file = path11.join(home, "last-update.json");
|
|
2529
|
+
const raw = await fs10.readFile(file, "utf8").catch(() => null);
|
|
2395
2530
|
if (raw === null) return null;
|
|
2396
|
-
await
|
|
2531
|
+
await fs10.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
2397
2532
|
return parseLastUpdate(raw);
|
|
2398
2533
|
}
|
|
2399
2534
|
function lastUpdateMessage(u, commits) {
|
|
@@ -2401,8 +2536,8 @@ function lastUpdateMessage(u, commits) {
|
|
|
2401
2536
|
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
2402
2537
|
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
2403
2538
|
}
|
|
2404
|
-
var OFFICE_DIR =
|
|
2405
|
-
var isSamePath = (a, b) => process.platform === "win32" ?
|
|
2539
|
+
var OFFICE_DIR = path11.resolve(import.meta.dirname, "..");
|
|
2540
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path11.resolve(a).toLowerCase() === path11.resolve(b).toLowerCase() : path11.resolve(a) === path11.resolve(b);
|
|
2406
2541
|
var realOffice = {
|
|
2407
2542
|
launcher: underLauncher(),
|
|
2408
2543
|
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
@@ -2416,6 +2551,340 @@ var realOffice = {
|
|
|
2416
2551
|
}
|
|
2417
2552
|
};
|
|
2418
2553
|
|
|
2554
|
+
// server/voice.ts
|
|
2555
|
+
import crypto5 from "node:crypto";
|
|
2556
|
+
import fs11 from "node:fs/promises";
|
|
2557
|
+
import path12 from "node:path";
|
|
2558
|
+
|
|
2559
|
+
// shared/speech.ts
|
|
2560
|
+
var SPEECH_MAX_CHARS = 1500;
|
|
2561
|
+
var SAMPLE_LINE = "Hi! This is how I'll sound when I message you from the office.";
|
|
2562
|
+
var EMOJI = /[\p{Extended_Pictographic}\p{Regional_Indicator}\u{1F3FB}-\u{1F3FF}︎️⃣]/gu;
|
|
2563
|
+
var URL2 = /\b(?:https?:\/\/|www\.)[^\s<>()]+[^\s<>().,;:!?'"]/gi;
|
|
2564
|
+
function speechText(text, max = SPEECH_MAX_CHARS) {
|
|
2565
|
+
let s2 = text.replace(/\r\n?/g, "\n");
|
|
2566
|
+
s2 = s2.replace(/```[\s\S]*?(```|$)/g, "\n");
|
|
2567
|
+
s2 = s2.replace(/`([^`\n]*)`/g, "$1");
|
|
2568
|
+
s2 = s2.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
2569
|
+
s2 = s2.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
2570
|
+
s2 = s2.replace(/<(https?:[^>\s]+)>/g, "$1");
|
|
2571
|
+
s2 = s2.replace(/<\/?[a-z][^>]*>/gi, "");
|
|
2572
|
+
s2 = s2.replace(URL2, "a link");
|
|
2573
|
+
s2 = s2.replace(EMOJI, "");
|
|
2574
|
+
s2 = s2.replace(/(\*\*|__)(.+?)\1/g, "$2");
|
|
2575
|
+
s2 = s2.replace(/(^|[^\w*])\*(?!\s)([^*\n]+?)\*(?!\w)/g, "$1$2");
|
|
2576
|
+
s2 = s2.replace(/(^|[^\w])_(?!\s)([^_\n]+?)_(?!\w)/g, "$1$2");
|
|
2577
|
+
s2 = s2.replace(/~~(.+?)~~/g, "$1");
|
|
2578
|
+
s2 = s2.replace(/#(\d+)\b/g, "number $1");
|
|
2579
|
+
const lines = [];
|
|
2580
|
+
for (let line of s2.split("\n")) {
|
|
2581
|
+
line = line.replace(/^\s*#{1,6}\s+/, "").replace(/^\s*>+\s?/, "").replace(/^\s*[-*+•]\s+/, "").replace(/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/, "").replace(/^\s*([-*_]\s*){3,}$/, "").replace(/\s*\|\s*/g, ", ").replace(/^[,\s]+|[,\s]+$/g, "").replace(/\s+/g, " ").trim();
|
|
2582
|
+
if (!line) continue;
|
|
2583
|
+
lines.push(/[.!?:;,…]$/.test(line) ? line : `${line}.`);
|
|
2584
|
+
}
|
|
2585
|
+
return cut(lines.join(" ").replace(/\s+([.,!?;:])/g, "$1"), max);
|
|
2586
|
+
}
|
|
2587
|
+
function cut(s2, max) {
|
|
2588
|
+
if (s2.length <= max) return s2;
|
|
2589
|
+
const head = s2.slice(0, max + 1);
|
|
2590
|
+
const ends = [...head.matchAll(/[.!?…](?=\s|$)/g)];
|
|
2591
|
+
const end = ends.length ? ends[ends.length - 1].index + 1 : -1;
|
|
2592
|
+
if (end >= max / 2) return s2.slice(0, end);
|
|
2593
|
+
const space = s2.lastIndexOf(" ", max);
|
|
2594
|
+
return `${s2.slice(0, space > 0 ? space : max).replace(/[,;:\s]+$/, "")}\u2026`;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// server/httpError.ts
|
|
2598
|
+
var HttpError = class extends Error {
|
|
2599
|
+
constructor(status, message) {
|
|
2600
|
+
super(message);
|
|
2601
|
+
this.status = status;
|
|
2602
|
+
}
|
|
2603
|
+
status;
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// server/voice.ts
|
|
2607
|
+
var DEFAULT_VOICE_MODEL = "eleven_flash_v2_5";
|
|
2608
|
+
var VOICES_TTL_MS = 10 * 6e4;
|
|
2609
|
+
var SYNTH_TIMEOUT_MS = 2e4;
|
|
2610
|
+
var CACHE_MAX_FILES = 200;
|
|
2611
|
+
var CACHE_MAX_AGE_MS = 7 * 24 * 60 * 6e4;
|
|
2612
|
+
var KEY_REJECTED_NOTE = "\u{1F507} ElevenLabs rejected the key, so messages aren't read aloud until you set a new one in the voice settings.";
|
|
2613
|
+
var voice = (id, name, description, accent = "") => ({
|
|
2614
|
+
id,
|
|
2615
|
+
name,
|
|
2616
|
+
category: "library",
|
|
2617
|
+
labels: { accent, gender: "", age: "", description, use_case: "conversational" },
|
|
2618
|
+
previewUrl: null,
|
|
2619
|
+
recommended: true
|
|
2620
|
+
});
|
|
2621
|
+
var RECOMMENDED_VOICES = [
|
|
2622
|
+
voice("OZ0L6eISlOejga3XjDFt", "Talia", "warm soft guide"),
|
|
2623
|
+
voice("l7kNoIfnJKPg7779LI2t", "Eddie", "helpful and comforting"),
|
|
2624
|
+
voice("AaOhDHYJ1XLZk74lXhdE", "Caleb", "trusted guide"),
|
|
2625
|
+
voice("gOupLcAkjEnguROwi4oS", "Darian", "warm grounded storyteller"),
|
|
2626
|
+
voice("BFd5oBc2DDna33pSi4Gf", "Alicia", "polished global anchor"),
|
|
2627
|
+
voice("QtY3JBOUKEB5xzrRfOKc", "Maisie", "friendly casual neighbor"),
|
|
2628
|
+
voice("FrS6cKLB1wg4WYgPa9GW", "Wyatt", "seasoned mentor"),
|
|
2629
|
+
voice("6WwXjDDEMyNmFG95zycZ", "Eldrin", "crisp baritone", "british")
|
|
2630
|
+
];
|
|
2631
|
+
var DEFAULT_VOICE = {
|
|
2632
|
+
provider: "off",
|
|
2633
|
+
voiceId: RECOMMENDED_VOICES[0].id,
|
|
2634
|
+
voiceName: RECOMMENDED_VOICES[0].name,
|
|
2635
|
+
model: DEFAULT_VOICE_MODEL,
|
|
2636
|
+
speakOffice: false
|
|
2637
|
+
};
|
|
2638
|
+
var VOICE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
2639
|
+
var MODEL_ID = /^[a-z0-9_.-]{1,64}$/;
|
|
2640
|
+
function voiceSettings(base, patch) {
|
|
2641
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
2642
|
+
const out = { ...base };
|
|
2643
|
+
if (p.provider === "off" || p.provider === "browser" || p.provider === "elevenlabs") out.provider = p.provider;
|
|
2644
|
+
if (typeof p.voiceId === "string" && p.voiceId.trim() && p.voiceId.length <= 200) out.voiceId = p.voiceId.trim();
|
|
2645
|
+
if (typeof p.voiceName === "string") out.voiceName = p.voiceName.trim().slice(0, 80);
|
|
2646
|
+
if (typeof p.model === "string" && MODEL_ID.test(p.model.trim())) out.model = p.model.trim();
|
|
2647
|
+
if (typeof p.speakOffice === "boolean") out.speakOffice = p.speakOffice;
|
|
2648
|
+
return out;
|
|
2649
|
+
}
|
|
2650
|
+
function cacheToPrune(files, now2, max = CACHE_MAX_FILES, maxAgeMs = CACHE_MAX_AGE_MS) {
|
|
2651
|
+
const fresh = files.filter((f) => now2 - f.mtimeMs <= maxAgeMs).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2652
|
+
const keep = new Set(fresh.slice(0, max).map((f) => f.name));
|
|
2653
|
+
return files.filter((f) => !keep.has(f.name)).map((f) => f.name);
|
|
2654
|
+
}
|
|
2655
|
+
function speaks(m, s2) {
|
|
2656
|
+
return m.from === "ceo" || m.from === "office" && s2.speakOffice;
|
|
2657
|
+
}
|
|
2658
|
+
var VoiceApiError = class extends Error {
|
|
2659
|
+
constructor(status, message) {
|
|
2660
|
+
super(message);
|
|
2661
|
+
this.status = status;
|
|
2662
|
+
}
|
|
2663
|
+
status;
|
|
2664
|
+
};
|
|
2665
|
+
var Voice = class {
|
|
2666
|
+
// secrets-file writes, one after another
|
|
2667
|
+
constructor(deps) {
|
|
2668
|
+
this.deps = deps;
|
|
2669
|
+
}
|
|
2670
|
+
deps;
|
|
2671
|
+
key = "";
|
|
2672
|
+
rejected = false;
|
|
2673
|
+
// ElevenLabs said 401: no more calls until the key changes
|
|
2674
|
+
voicesCache = null;
|
|
2675
|
+
making = /* @__PURE__ */ new Map();
|
|
2676
|
+
queue = Promise.resolve();
|
|
2677
|
+
writing = Promise.resolve();
|
|
2678
|
+
async init() {
|
|
2679
|
+
const s2 = await this.readSecrets();
|
|
2680
|
+
this.key = typeof s2.elevenlabsKey === "string" ? s2.elevenlabsKey : "";
|
|
2681
|
+
this.rejected = !!this.key && s2.elevenlabsKeyRejected === true;
|
|
2682
|
+
await this.prune().catch(() => void 0);
|
|
2683
|
+
}
|
|
2684
|
+
keyView() {
|
|
2685
|
+
return { voiceKeySet: !!this.key, voiceKeyHint: this.key.slice(-4) };
|
|
2686
|
+
}
|
|
2687
|
+
/** PUT /api/voice/key: '' removes the key; anything else must pass one cheap ElevenLabs call first. */
|
|
2688
|
+
async setKey(raw) {
|
|
2689
|
+
const key = raw.trim();
|
|
2690
|
+
if (key) {
|
|
2691
|
+
if (key.length > 200 || /\s/.test(key)) throw new HttpError(400, "That doesn't look like an ElevenLabs API key.");
|
|
2692
|
+
try {
|
|
2693
|
+
await this.deps.api.checkKey(key);
|
|
2694
|
+
} catch (err) {
|
|
2695
|
+
if (err instanceof VoiceApiError && err.status === 401) throw new HttpError(400, "ElevenLabs rejected that key. Copy it again from elevenlabs.io \u2192 Developers \u2192 API Keys.");
|
|
2696
|
+
if (err instanceof VoiceApiError && err.status === 403) throw new HttpError(400, "That key may not read voices. Give it the Voices (read) and Text to Speech permissions on elevenlabs.io.");
|
|
2697
|
+
throw new HttpError(502, `Couldn't check the key with ElevenLabs: ${err.message}`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
this.key = key;
|
|
2701
|
+
this.rejected = false;
|
|
2702
|
+
this.voicesCache = null;
|
|
2703
|
+
await this.writeSecrets({ elevenlabsKey: key || void 0, elevenlabsKeyRejected: void 0 });
|
|
2704
|
+
const view2 = this.keyView();
|
|
2705
|
+
this.deps.keyChanged(view2);
|
|
2706
|
+
return view2;
|
|
2707
|
+
}
|
|
2708
|
+
/** GET /api/voice/voices: the account's voices plus the shortlist, shortlist first. Cached for 10 minutes. */
|
|
2709
|
+
async voices() {
|
|
2710
|
+
this.requireKey();
|
|
2711
|
+
const now2 = this.now();
|
|
2712
|
+
if (this.voicesCache && now2 - this.voicesCache.at < VOICES_TTL_MS) return this.voicesCache.list;
|
|
2713
|
+
const key = this.key;
|
|
2714
|
+
const mine = await this.deps.api.listVoices(key).catch((err) => this.failed(err, key, "list the voices"));
|
|
2715
|
+
const recommended = new Map(RECOMMENDED_VOICES.map((v) => [v.id, v]));
|
|
2716
|
+
const list = [
|
|
2717
|
+
...RECOMMENDED_VOICES.map((r) => {
|
|
2718
|
+
const own = mine.find((v) => v.id === r.id);
|
|
2719
|
+
return own ? { ...own, labels: fill(own.labels, r.labels), recommended: true } : r;
|
|
2720
|
+
}),
|
|
2721
|
+
...mine.filter((v) => !recommended.has(v.id)).map((v) => ({ ...v, recommended: false })).sort((a, b) => a.name.localeCompare(b.name))
|
|
2722
|
+
];
|
|
2723
|
+
if (this.key === key) this.voicesCache = { at: now2, list };
|
|
2724
|
+
return list;
|
|
2725
|
+
}
|
|
2726
|
+
/** GET /api/voice/messages/:id: a phone message as mp3, made once and cached. */
|
|
2727
|
+
async messageAudio(id) {
|
|
2728
|
+
const s2 = this.deps.settings();
|
|
2729
|
+
if (s2.provider !== "elevenlabs") throw new HttpError(404, "Messages aren't read by ElevenLabs right now.");
|
|
2730
|
+
const m = this.deps.message(id);
|
|
2731
|
+
if (!m || !speaks(m, s2)) throw new HttpError(404, `No spoken message ${id}`);
|
|
2732
|
+
const text = speechText(m.text);
|
|
2733
|
+
if (!text) throw new HttpError(404, `Message ${id} has nothing to say aloud`);
|
|
2734
|
+
return this.audio(String(m.id), s2.voiceId, s2.model, text);
|
|
2735
|
+
}
|
|
2736
|
+
/** GET /api/voice/sample: the settings' Test button, in `voiceId` (default: the chosen voice). */
|
|
2737
|
+
async sampleAudio(voiceId) {
|
|
2738
|
+
const s2 = this.deps.settings();
|
|
2739
|
+
return this.audio("sample", voiceId || s2.voiceId, s2.model, SAMPLE_LINE);
|
|
2740
|
+
}
|
|
2741
|
+
async audio(name, voiceId, model, text) {
|
|
2742
|
+
if (!VOICE_ID.test(voiceId)) throw new HttpError(400, "Pick an ElevenLabs voice first.");
|
|
2743
|
+
const hash = crypto5.createHash("sha256").update(`${voiceId}
|
|
2744
|
+
${model}
|
|
2745
|
+
${text}`).digest("hex").slice(0, 12);
|
|
2746
|
+
const file = path12.join(this.deps.cacheDir, `${name}-${hash}.mp3`);
|
|
2747
|
+
const cached = await fs11.readFile(file).catch(() => null);
|
|
2748
|
+
if (cached) return cached;
|
|
2749
|
+
this.requireKey();
|
|
2750
|
+
const busy = this.making.get(file);
|
|
2751
|
+
if (busy) return busy;
|
|
2752
|
+
const made = this.oneAtATime(() => this.synthesize(file, { voiceId, model, text })).finally(() => this.making.delete(file));
|
|
2753
|
+
this.making.set(file, made);
|
|
2754
|
+
return made;
|
|
2755
|
+
}
|
|
2756
|
+
oneAtATime(fn) {
|
|
2757
|
+
const run2 = this.queue.then(fn, fn);
|
|
2758
|
+
this.queue = run2.catch(() => void 0);
|
|
2759
|
+
return run2;
|
|
2760
|
+
}
|
|
2761
|
+
async synthesize(file, req) {
|
|
2762
|
+
this.requireKey();
|
|
2763
|
+
const key = this.key;
|
|
2764
|
+
const audio = await this.deps.api.synthesize(key, req, AbortSignal.timeout(SYNTH_TIMEOUT_MS)).catch((err) => this.failed(err, key, "speak the message"));
|
|
2765
|
+
await fs11.mkdir(this.deps.cacheDir, { recursive: true });
|
|
2766
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2767
|
+
await fs11.writeFile(tmp, audio);
|
|
2768
|
+
await fs11.rename(tmp, file);
|
|
2769
|
+
await this.prune().catch(() => void 0);
|
|
2770
|
+
return audio;
|
|
2771
|
+
}
|
|
2772
|
+
/** An ElevenLabs failure as a short 502. A 401 also stops every call until the key changes, and says so once. */
|
|
2773
|
+
async failed(err, key, what) {
|
|
2774
|
+
if (err instanceof VoiceApiError && err.status === 401 && key === this.key && !this.rejected) {
|
|
2775
|
+
this.rejected = true;
|
|
2776
|
+
this.deps.officeNote(KEY_REJECTED_NOTE);
|
|
2777
|
+
await this.writeSecrets({ elevenlabsKeyRejected: true }).catch(() => void 0);
|
|
2778
|
+
}
|
|
2779
|
+
if (err instanceof VoiceApiError && err.status === 401) throw new HttpError(502, "ElevenLabs rejected the key.");
|
|
2780
|
+
throw new HttpError(502, `ElevenLabs couldn't ${what}: ${err.message}`.slice(0, 240));
|
|
2781
|
+
}
|
|
2782
|
+
requireKey() {
|
|
2783
|
+
if (!this.key) throw new HttpError(409, "Add an ElevenLabs API key first.");
|
|
2784
|
+
if (this.rejected) throw new HttpError(502, "ElevenLabs rejected the key. Set a new one in the voice settings.");
|
|
2785
|
+
}
|
|
2786
|
+
async prune() {
|
|
2787
|
+
const names = (await fs11.readdir(this.deps.cacheDir).catch(() => [])).filter((n) => n.endsWith(".mp3"));
|
|
2788
|
+
const files = await Promise.all(names.map(async (name) => ({ name, mtimeMs: (await fs11.stat(path12.join(this.deps.cacheDir, name)).catch(() => null))?.mtimeMs ?? 0 })));
|
|
2789
|
+
for (const name of cacheToPrune(files, this.now())) await fs11.rm(path12.join(this.deps.cacheDir, name), { force: true, maxRetries: 3 });
|
|
2790
|
+
}
|
|
2791
|
+
now() {
|
|
2792
|
+
return this.deps.now?.() ?? Date.now();
|
|
2793
|
+
}
|
|
2794
|
+
async readSecrets() {
|
|
2795
|
+
try {
|
|
2796
|
+
const s2 = JSON.parse(await fs11.readFile(this.deps.secretsFile, "utf8"));
|
|
2797
|
+
return s2 && typeof s2 === "object" ? s2 : {};
|
|
2798
|
+
} catch {
|
|
2799
|
+
return {};
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
/** Merge into the secrets file (other secrets stay), owner-only where the OS has file modes. */
|
|
2803
|
+
writeSecrets(patch) {
|
|
2804
|
+
const write = this.writing.then(() => this.mergeSecrets(patch));
|
|
2805
|
+
this.writing = write.catch(() => void 0);
|
|
2806
|
+
return write;
|
|
2807
|
+
}
|
|
2808
|
+
async mergeSecrets(patch) {
|
|
2809
|
+
const next = { ...await this.readSecrets(), ...patch };
|
|
2810
|
+
for (const k of Object.keys(next)) if (next[k] === void 0) delete next[k];
|
|
2811
|
+
await fs11.mkdir(path12.dirname(this.deps.secretsFile), { recursive: true });
|
|
2812
|
+
const tmp = `${this.deps.secretsFile}.tmp`;
|
|
2813
|
+
await fs11.writeFile(tmp, JSON.stringify(next, null, 2), { mode: 384 });
|
|
2814
|
+
await fs11.rename(tmp, this.deps.secretsFile);
|
|
2815
|
+
await fs11.chmod(this.deps.secretsFile, 384).catch(() => void 0);
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
function fill(own, ours) {
|
|
2819
|
+
return { ...own, description: own.description || ours.description };
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// server/elevenlabs.ts
|
|
2823
|
+
var API = "https://api.elevenlabs.io";
|
|
2824
|
+
var CHECK_TIMEOUT_MS = 1e4;
|
|
2825
|
+
var MAX_VOICE_PAGES = 5;
|
|
2826
|
+
async function call(key, pathname, init, signal) {
|
|
2827
|
+
let res;
|
|
2828
|
+
try {
|
|
2829
|
+
res = await fetch(`${API}${pathname}`, { ...init, signal, headers: { ...init.headers, "xi-api-key": key } });
|
|
2830
|
+
} catch (err) {
|
|
2831
|
+
throw new VoiceApiError(0, signal.aborted ? "it took too long to answer" : `it couldn't be reached (${err.message})`);
|
|
2832
|
+
}
|
|
2833
|
+
if (!res.ok) throw new VoiceApiError(res.status, await reason(res));
|
|
2834
|
+
return res;
|
|
2835
|
+
}
|
|
2836
|
+
async function reason(res) {
|
|
2837
|
+
const body2 = await res.text().catch(() => "");
|
|
2838
|
+
let text = "";
|
|
2839
|
+
try {
|
|
2840
|
+
const d = JSON.parse(body2).detail;
|
|
2841
|
+
if (typeof d === "string") text = d;
|
|
2842
|
+
else if (Array.isArray(d)) text = d.map((x) => x?.msg).filter(Boolean).join("; ");
|
|
2843
|
+
else if (d && typeof d === "object") text = String(d.message ?? d.status ?? "");
|
|
2844
|
+
} catch {
|
|
2845
|
+
}
|
|
2846
|
+
return `${res.status}${text ? `: ${text}` : ""}`.slice(0, 200);
|
|
2847
|
+
}
|
|
2848
|
+
var s = (v) => typeof v === "string" ? v : "";
|
|
2849
|
+
function voiceOption(v) {
|
|
2850
|
+
const l = v.labels ?? {};
|
|
2851
|
+
return {
|
|
2852
|
+
id: s(v.voice_id),
|
|
2853
|
+
name: s(v.name),
|
|
2854
|
+
category: s(v.category),
|
|
2855
|
+
labels: { accent: s(l.accent), gender: s(l.gender), age: s(l.age), description: s(l.description) || s(l.descriptive), use_case: s(l.use_case) || s(l["use case"]) },
|
|
2856
|
+
previewUrl: s(v.preview_url) || null
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
var elevenLabs = {
|
|
2860
|
+
// The cheapest call a voices-reading key can make: one voice.
|
|
2861
|
+
async checkKey(key) {
|
|
2862
|
+
await call(key, "/v2/voices?page_size=1&include_total_count=false", {}, AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2863
|
+
},
|
|
2864
|
+
async listVoices(key) {
|
|
2865
|
+
const out = [];
|
|
2866
|
+
let token = "";
|
|
2867
|
+
for (let page = 0; page < MAX_VOICE_PAGES; page++) {
|
|
2868
|
+
const q = new URLSearchParams({ page_size: "100", include_total_count: "false", ...token ? { next_page_token: token } : {} });
|
|
2869
|
+
const res = await call(key, `/v2/voices?${q}`, {}, AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2870
|
+
const body2 = await res.json();
|
|
2871
|
+
for (const v of body2.voices ?? []) if (s(v.voice_id)) out.push(voiceOption(v));
|
|
2872
|
+
token = s(body2.next_page_token);
|
|
2873
|
+
if (!body2.has_more || !token) break;
|
|
2874
|
+
}
|
|
2875
|
+
return out;
|
|
2876
|
+
},
|
|
2877
|
+
async synthesize(key, { voiceId, model, text }, signal) {
|
|
2878
|
+
const res = await call(
|
|
2879
|
+
key,
|
|
2880
|
+
`/v1/text-to-speech/${encodeURIComponent(voiceId)}?output_format=mp3_44100_128`,
|
|
2881
|
+
{ method: "POST", headers: { "Content-Type": "application/json", Accept: "audio/mpeg" }, body: JSON.stringify({ text, model_id: model }) },
|
|
2882
|
+
signal
|
|
2883
|
+
);
|
|
2884
|
+
return Buffer.from(await res.arrayBuffer());
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2419
2888
|
// server/backend.ts
|
|
2420
2889
|
var realBackend = {
|
|
2421
2890
|
demo: false,
|
|
@@ -2446,6 +2915,7 @@ var realBackend = {
|
|
|
2446
2915
|
mainDir,
|
|
2447
2916
|
deskDir,
|
|
2448
2917
|
prepareDesk,
|
|
2918
|
+
installDeps: (dir, cb) => installDesk(dir, cb),
|
|
2449
2919
|
removeDesk,
|
|
2450
2920
|
sweepDesks,
|
|
2451
2921
|
releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
|
|
@@ -2456,22 +2926,23 @@ var realBackend = {
|
|
|
2456
2926
|
releaseClis,
|
|
2457
2927
|
detectClis,
|
|
2458
2928
|
previews: realPreviews,
|
|
2459
|
-
office: realOffice
|
|
2929
|
+
office: realOffice,
|
|
2930
|
+
voice: elevenLabs
|
|
2460
2931
|
};
|
|
2461
2932
|
|
|
2462
2933
|
// server/demo.ts
|
|
2463
|
-
import
|
|
2934
|
+
import crypto6 from "node:crypto";
|
|
2464
2935
|
import http from "node:http";
|
|
2465
|
-
import
|
|
2936
|
+
import path13 from "node:path";
|
|
2466
2937
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
2467
|
-
function issue(n, title,
|
|
2468
|
-
return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
2938
|
+
function issue(n, title, body2, fullName, labels = []) {
|
|
2939
|
+
return { number: n, title, body: body2, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
2469
2940
|
}
|
|
2470
2941
|
var repos = /* @__PURE__ */ new Map();
|
|
2471
2942
|
var bareRepos = /* @__PURE__ */ new Set();
|
|
2472
2943
|
var mergedSinceSync = /* @__PURE__ */ new Map();
|
|
2473
2944
|
var closedIssues = /* @__PURE__ */ new Set();
|
|
2474
|
-
var fakeSha = () =>
|
|
2945
|
+
var fakeSha = () => crypto6.randomBytes(20).toString("hex");
|
|
2475
2946
|
function runChecks(pr, fail = Math.random() < 0.2) {
|
|
2476
2947
|
Object.assign(pr, { checks: "pending", pendingChecks: ["CI / build", "Vercel"], failedChecks: [] });
|
|
2477
2948
|
setTimeout(() => {
|
|
@@ -2484,7 +2955,7 @@ function runChecks(pr, fail = Math.random() < 0.2) {
|
|
|
2484
2955
|
}
|
|
2485
2956
|
function seed(fullName, description, titles) {
|
|
2486
2957
|
const r = { fullName, description, issues: [], pulls: [], nextNumber: 1 };
|
|
2487
|
-
for (const [title,
|
|
2958
|
+
for (const [title, body2] of titles) r.issues.push(issue(r.nextNumber++, title, body2, fullName));
|
|
2488
2959
|
repos.set(fullName, r);
|
|
2489
2960
|
}
|
|
2490
2961
|
seed("demo-co/pixel-todo", "A cheerful todo app", [
|
|
@@ -2501,7 +2972,7 @@ seed("demo-co/weather-api", "Tiny weather REST API", [
|
|
|
2501
2972
|
["OpenAPI spec", "Publish an OpenAPI 3.1 document at /openapi.json."]
|
|
2502
2973
|
]);
|
|
2503
2974
|
function screenshotSvg(title, url, hue) {
|
|
2504
|
-
const safe = (
|
|
2975
|
+
const safe = (s2) => s2.replace(/[<>&"]/g, "");
|
|
2505
2976
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="400" viewBox="0 0 640 400">
|
|
2506
2977
|
<rect width="640" height="400" fill="hsl(${hue},60%,97%)"/>
|
|
2507
2978
|
<rect width="640" height="56" fill="hsl(${hue},70%,55%)"/>
|
|
@@ -2515,7 +2986,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
2515
2986
|
</svg>`;
|
|
2516
2987
|
}
|
|
2517
2988
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
2518
|
-
const branch =
|
|
2989
|
+
const branch = path13.basename(opts.cwd);
|
|
2519
2990
|
const hue = issueNumber * 67 % 360;
|
|
2520
2991
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
2521
2992
|
const port = 5200 + issueNumber % 50;
|
|
@@ -2551,18 +3022,25 @@ function devScript(opts, cb, issueNumber, issueTitle) {
|
|
|
2551
3022
|
[{ kind: "tool", tool: "Bash", text: `\u23FA $ gh pr create --title "${issueTitle}" --body "Closes #${issueNumber}"` }]
|
|
2552
3023
|
];
|
|
2553
3024
|
}
|
|
2554
|
-
function qaScript(cb, pr, title, round) {
|
|
3025
|
+
function qaScript(cb, pr, title, round, checks) {
|
|
2555
3026
|
const port = 5600 + pr % 50;
|
|
2556
3027
|
const hue = pr * 41 % 360;
|
|
2557
|
-
|
|
2558
|
-
[{ kind: "text", text: `\u25CF Testing PR #${pr} "${title}" (round ${round}). First, the acceptance criteria from the issue.` }],
|
|
2559
|
-
[{ kind: "tool", tool: "Bash", text: "\u23FA $ git diff origin/main...HEAD --stat" }, { kind: "result", text: " \u23BF 3 files changed, 82 insertions(+), 9 deletions(-)" }],
|
|
3028
|
+
const verify = checks.endsWith(": none") ? [
|
|
2560
3029
|
[{ kind: "tool", tool: "Bash", text: "\u23FA $ npm ci && npm test -- --run" }],
|
|
2561
3030
|
[
|
|
2562
3031
|
{ kind: "result", text: " \u23BF Test Files 8 passed (8)" },
|
|
2563
3032
|
{ kind: "result", text: " Tests 41 passed (41)" }
|
|
2564
3033
|
],
|
|
2565
|
-
[{ kind: "tool", tool: "Bash", text: "\u23FA $ npm run lint && npm run build" }, { kind: "result", text: " \u23BF \u2713 built in 1.84s" }]
|
|
3034
|
+
[{ kind: "tool", tool: "Bash", text: "\u23FA $ npm run lint && npm run build" }, { kind: "result", text: " \u23BF \u2713 built in 1.84s" }]
|
|
3035
|
+
] : [
|
|
3036
|
+
[{ kind: "tool", tool: "Read", text: "\u23FA Read .github/workflows/ci.yml" }, { kind: "result", text: " \u23BF Read 41 lines" }],
|
|
3037
|
+
[{ kind: "text", text: "\u25CF CI already runs the tests, lint and build, so I only need a build to start the app." }],
|
|
3038
|
+
[{ kind: "tool", tool: "Bash", text: "\u23FA $ npm ci && npm run build" }, { kind: "result", text: " \u23BF \u2713 built in 1.84s" }]
|
|
3039
|
+
];
|
|
3040
|
+
return [
|
|
3041
|
+
[{ kind: "text", text: `\u25CF Testing PR #${pr} "${title}" (round ${round}). ${checks}. First, the acceptance criteria from the issue.` }],
|
|
3042
|
+
[{ kind: "tool", tool: "Bash", text: "\u23FA $ git diff origin/main...HEAD --stat" }, { kind: "result", text: " \u23BF 3 files changed, 82 insertions(+), 9 deletions(-)" }],
|
|
3043
|
+
...verify,
|
|
2566
3044
|
[{ kind: "tool", tool: "Bash", text: `\u23FA $ npm run preview -- --port ${port} &` }, { kind: "result", text: ` \u23BF Local: http://localhost:${port}/` }],
|
|
2567
3045
|
() => cb.browserUrl(`http://localhost:${port}/`),
|
|
2568
3046
|
[{ kind: "tool", tool: "mcp__playwright__browser_navigate", text: `\u23FA \u{1F310} navigate http://localhost:${port}/` }, { kind: "result", text: ` \u23BF Page URL: http://localhost:${port}/` }],
|
|
@@ -2598,8 +3076,9 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2598
3076
|
{ kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort` },
|
|
2599
3077
|
{ kind: "system", text: ` cwd ${opts.cwd}` }
|
|
2600
3078
|
];
|
|
2601
|
-
const
|
|
2602
|
-
const
|
|
3079
|
+
const checks = opts.prompt.match(/^GitHub checks right now: .*$/m)?.[0] ?? "GitHub checks right now: none";
|
|
3080
|
+
const body2 = kind === "qa" ? qaScript(cb, number, title, round, checks) : kind === "fix" ? fixScript(number, pushes) : devScript(opts, cb, number, title);
|
|
3081
|
+
const script = [header, ...body2];
|
|
2603
3082
|
const finish = () => {
|
|
2604
3083
|
const costUsd = 0.3 + Math.random();
|
|
2605
3084
|
const turns = 15 + Math.floor(Math.random() * 20);
|
|
@@ -2654,7 +3133,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2654
3133
|
number: n,
|
|
2655
3134
|
title,
|
|
2656
3135
|
url,
|
|
2657
|
-
headRefName: `swarm/issue-${number}-${
|
|
3136
|
+
headRefName: `swarm/issue-${number}-${path13.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
2658
3137
|
state: "OPEN",
|
|
2659
3138
|
isDraft: false,
|
|
2660
3139
|
mergeable: "MERGEABLE",
|
|
@@ -2680,12 +3159,12 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2680
3159
|
const step = () => {
|
|
2681
3160
|
if (stopped) return;
|
|
2682
3161
|
if (i >= script.length) return finish();
|
|
2683
|
-
const
|
|
2684
|
-
if (typeof
|
|
3162
|
+
const s2 = script[i++];
|
|
3163
|
+
if (typeof s2 === "function") s2();
|
|
2685
3164
|
else {
|
|
2686
|
-
const tool2 =
|
|
3165
|
+
const tool2 = s2.find((e) => e.kind === "tool");
|
|
2687
3166
|
cb.tool(tool2?.tool ?? null);
|
|
2688
|
-
cb.log(
|
|
3167
|
+
cb.log(s2);
|
|
2689
3168
|
}
|
|
2690
3169
|
timers.push(setTimeout(step, 1600 + Math.random() * 3800));
|
|
2691
3170
|
};
|
|
@@ -2783,6 +3262,7 @@ function fakeUsageWarning(cb) {
|
|
|
2783
3262
|
}
|
|
2784
3263
|
function createDemoBackend() {
|
|
2785
3264
|
const deskRepo = /* @__PURE__ */ new Map();
|
|
3265
|
+
const installedDesks = /* @__PURE__ */ new Set();
|
|
2786
3266
|
const folders = /* @__PURE__ */ new Map();
|
|
2787
3267
|
const addFolder = (name, github, git2 = true) => folders.set(name, { name, path: `/demo/projects/${name}`, git: git2, github, modified: Date.now() - folders.size * 36e5 });
|
|
2788
3268
|
for (const r of repos.values()) addFolder(r.fullName.split("/")[1], r.fullName);
|
|
@@ -2824,11 +3304,11 @@ function createDemoBackend() {
|
|
|
2824
3304
|
},
|
|
2825
3305
|
listIssues: async (fullName) => [...repos.get(fullName)?.issues ?? []],
|
|
2826
3306
|
listPulls: async (fullName) => [...repos.get(fullName)?.pulls ?? []],
|
|
2827
|
-
createIssue: async (fullName, title,
|
|
3307
|
+
createIssue: async (fullName, title, body2, labels = []) => {
|
|
2828
3308
|
const r = repos.get(fullName);
|
|
2829
3309
|
if (!r) throw new Error("Unknown repo");
|
|
2830
3310
|
const n = r.nextNumber++;
|
|
2831
|
-
r.issues.push(issue(n, title,
|
|
3311
|
+
r.issues.push(issue(n, title, body2, fullName, labels));
|
|
2832
3312
|
return n;
|
|
2833
3313
|
},
|
|
2834
3314
|
issueState: async (fullName, number) => {
|
|
@@ -2887,7 +3367,11 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2887
3367
|
closesIssues: pr.closesIssues,
|
|
2888
3368
|
state: pr.state,
|
|
2889
3369
|
mergeable: pr.mergeable,
|
|
2890
|
-
mergeState: pr.mergeState
|
|
3370
|
+
mergeState: pr.mergeState,
|
|
3371
|
+
checks: pr.checks,
|
|
3372
|
+
checkNames: pr.checks === "none" ? [] : ["CI / build", "Vercel"],
|
|
3373
|
+
failedChecks: pr.failedChecks.map((c) => c.name),
|
|
3374
|
+
pendingChecks: pr.pendingChecks
|
|
2891
3375
|
};
|
|
2892
3376
|
},
|
|
2893
3377
|
issueDetails: async (fullName, number) => {
|
|
@@ -2912,6 +3396,18 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2912
3396
|
deskRepo.set(dir, fullName);
|
|
2913
3397
|
return dir;
|
|
2914
3398
|
},
|
|
3399
|
+
installDeps: async (dir, cb) => {
|
|
3400
|
+
if (installedDesks.has(dir)) {
|
|
3401
|
+
cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
3402
|
+
return "skipped";
|
|
3403
|
+
}
|
|
3404
|
+
cb.log(["Installing dependencies\u2026", "$ npm ci"]);
|
|
3405
|
+
cb.installing();
|
|
3406
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
3407
|
+
cb.log(["Dependencies installed."]);
|
|
3408
|
+
installedDesks.add(dir);
|
|
3409
|
+
return "installed";
|
|
3410
|
+
},
|
|
2915
3411
|
removeDesk: async () => void 0,
|
|
2916
3412
|
sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
|
|
2917
3413
|
releaseDesk: async () => void 0,
|
|
@@ -2926,7 +3422,8 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2926
3422
|
releaseClis: async () => void 0,
|
|
2927
3423
|
detectClis: async () => CLIS.map((c) => ({ id: c.id, label: c.label, installed: true, version: "demo", integrated: c.integrated })),
|
|
2928
3424
|
previews: demoPreviews,
|
|
2929
|
-
office: demoOffice
|
|
3425
|
+
office: demoOffice,
|
|
3426
|
+
voice: demoVoice
|
|
2930
3427
|
};
|
|
2931
3428
|
}
|
|
2932
3429
|
var OFFICE_REPO = "demo-co/pixel-todo";
|
|
@@ -3031,11 +3528,11 @@ var demoPreviews = {
|
|
|
3031
3528
|
async stop() {
|
|
3032
3529
|
stopped = true;
|
|
3033
3530
|
timers.forEach(clearTimeout);
|
|
3034
|
-
const
|
|
3531
|
+
const s2 = server2;
|
|
3035
3532
|
server2 = null;
|
|
3036
|
-
if (!
|
|
3037
|
-
|
|
3038
|
-
await new Promise((resolve) =>
|
|
3533
|
+
if (!s2?.listening) return;
|
|
3534
|
+
s2.closeAllConnections();
|
|
3535
|
+
await new Promise((resolve) => s2.close(() => resolve()));
|
|
3039
3536
|
}
|
|
3040
3537
|
};
|
|
3041
3538
|
}
|
|
@@ -3107,12 +3604,12 @@ function ceoSession(opts, cb) {
|
|
|
3107
3604
|
};
|
|
3108
3605
|
const status = async () => {
|
|
3109
3606
|
await step([{ kind: "tool", tool: "mcp__office__company_status", text: `\u23FA ${describeOfficeTool("company_status", {})}` }]);
|
|
3110
|
-
const
|
|
3111
|
-
const people =
|
|
3112
|
-
const issues =
|
|
3113
|
-
const free =
|
|
3114
|
-
cb.log([{ kind: "result", text: ` \u23BF ${
|
|
3115
|
-
return
|
|
3607
|
+
const s2 = JSON.parse(await office.call("company_status", {}));
|
|
3608
|
+
const people = s2.floors.reduce((n, f) => n + f.team.length, 0);
|
|
3609
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3610
|
+
const free = s2.floors.reduce((n, f) => n + f.seats.dev.free + f.seats.qa.free, 0);
|
|
3611
|
+
cb.log([{ kind: "result", text: ` \u23BF ${s2.floors.length} floors \xB7 ${people} people \xB7 ${free} free seats \xB7 ${issues} open issues \xB7 ${s2.pendingProposals.length} proposals pending` }]);
|
|
3612
|
+
return s2;
|
|
3116
3613
|
};
|
|
3117
3614
|
const use = async (name, args) => {
|
|
3118
3615
|
await step([{ kind: "tool", tool: `mcp__office__${name}`, text: `\u23FA ${describeOfficeTool(name, args)}` }]);
|
|
@@ -3122,7 +3619,7 @@ function ceoSession(opts, cb) {
|
|
|
3122
3619
|
};
|
|
3123
3620
|
const read = (file, lines) => step([{ kind: "tool", tool: "Read", text: `\u23FA Read ${file}` }, { kind: "result", text: ` \u23BF Read ${lines} lines` }]);
|
|
3124
3621
|
const think = (text) => step([{ kind: "thinking", text: "\u273B Thinking\u2026" }, { kind: "text", text: `\u25CF ${text}` }], 1800);
|
|
3125
|
-
const short = (
|
|
3622
|
+
const short = (s2, n = 48) => s2.length > n ? `${s2.slice(0, n - 1).trim()}\u2026` : s2;
|
|
3126
3623
|
const planIssues = async (floor, mission, team) => {
|
|
3127
3624
|
const first = await use("file_issue", {
|
|
3128
3625
|
floor,
|
|
@@ -3165,8 +3662,8 @@ Acceptance criteria:
|
|
|
3165
3662
|
};
|
|
3166
3663
|
const scripts = {
|
|
3167
3664
|
async onboard(floor, fullName) {
|
|
3168
|
-
const
|
|
3169
|
-
const f =
|
|
3665
|
+
const s2 = await status();
|
|
3666
|
+
const f = s2.floors.find((x) => x.floor === floor);
|
|
3170
3667
|
if (!f) return "That floor has gone, so there was nothing to onboard.";
|
|
3171
3668
|
const p = PROFILES[fullName.split("/")[1] ?? ""] ?? GENERIC;
|
|
3172
3669
|
await read(`${f.repo}/README.md`, 48);
|
|
@@ -3198,8 +3695,8 @@ Acceptance criteria:
|
|
|
3198
3695
|
].filter(Boolean).join(" ");
|
|
3199
3696
|
},
|
|
3200
3697
|
async plan(floor, mission) {
|
|
3201
|
-
const
|
|
3202
|
-
const f =
|
|
3698
|
+
const s2 = await status();
|
|
3699
|
+
const f = s2.floors.find((x) => x.floor === floor);
|
|
3203
3700
|
if (!f) return "That floor has gone, so there was nothing to plan.";
|
|
3204
3701
|
await read(`${f.repo}/README.md`, 12);
|
|
3205
3702
|
await think("Foundation first, so the parallel work doesn't collide.");
|
|
@@ -3207,9 +3704,9 @@ Acceptance criteria:
|
|
|
3207
3704
|
return `I turned the brief into three issues on floor ${floor}. #${n} sets up the skeleton; the other two say "Depends on #${n}", so nobody starts them early.`;
|
|
3208
3705
|
},
|
|
3209
3706
|
async review() {
|
|
3210
|
-
const
|
|
3707
|
+
const s2 = await status();
|
|
3211
3708
|
await think("Checking each floor for idle people and stuck work.");
|
|
3212
|
-
for (const f of
|
|
3709
|
+
for (const f of s2.floors) {
|
|
3213
3710
|
const idle = f.team.filter((a) => a.role === "dev" && !a.specialty && (a.status === "idle" || a.status === "done"));
|
|
3214
3711
|
const devs = f.team.filter((a) => a.role === "dev").length;
|
|
3215
3712
|
if (devs >= 6 && idle.length >= 2 && f.backlog.length < devs) {
|
|
@@ -3217,26 +3714,26 @@ Acceptance criteria:
|
|
|
3217
3714
|
return `Floor ${f.floor} is overstaffed: ${devs} developers for ${f.backlog.length} open issues. I suggest letting ${idle[idle.length - 1].name} go; it's on your phone.`;
|
|
3218
3715
|
}
|
|
3219
3716
|
}
|
|
3220
|
-
for (const f of
|
|
3717
|
+
for (const f of s2.floors) {
|
|
3221
3718
|
const specialist = f.team.find((a) => a.role === "dev" && a.specialty);
|
|
3222
3719
|
const unrouted = f.backlog.find((i) => !i.specialty && !i.inProgress);
|
|
3223
3720
|
if (!specialist || !unrouted) continue;
|
|
3224
3721
|
const out = await use("route_issue", { floor: f.floor, number: unrouted.number, specialty: specialist.specialty });
|
|
3225
3722
|
if (!out.startsWith("Refused")) return `Floor ${f.floor}: #${unrouted.number} had no specialty, so I routed it to ${specialist.specialty}, ${specialist.name}'s lane.`;
|
|
3226
3723
|
}
|
|
3227
|
-
const issues =
|
|
3228
|
-
const prs =
|
|
3229
|
-
return `All ${
|
|
3724
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3725
|
+
const prs = s2.floors.reduce((n, f) => n + f.pullRequests.length, 0);
|
|
3726
|
+
return `All ${s2.floors.length} floors look healthy: ${issues} open issues and ${prs} pull requests in flight. No changes needed.`;
|
|
3230
3727
|
},
|
|
3231
3728
|
async chat(text) {
|
|
3232
|
-
const
|
|
3729
|
+
const s2 = await status();
|
|
3233
3730
|
await think("Reading your message.");
|
|
3234
|
-
const people =
|
|
3235
|
-
const issues =
|
|
3236
|
-
const pending2 =
|
|
3237
|
-
const rows =
|
|
3731
|
+
const people = s2.floors.reduce((n, f) => n + f.team.length, 0);
|
|
3732
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3733
|
+
const pending2 = s2.pendingProposals.length;
|
|
3734
|
+
const rows = s2.floors.map((f) => `| ${f.floor} | \`${f.repo.split("/").pop()}\` | ${f.team.length} | ${f.backlog.length} | ${f.pullRequests.length} |`);
|
|
3238
3735
|
return [
|
|
3239
|
-
`**Quick status:** ${
|
|
3736
|
+
`**Quick status:** ${s2.floors.length} floor${s2.floors.length === 1 ? "" : "s"}, ${people} people and ${issues} open issues.`,
|
|
3240
3737
|
"",
|
|
3241
3738
|
"- The team is *heads down* on the backlog",
|
|
3242
3739
|
`- ${pending2 ? `**${pending2}** proposal${pending2 === 1 ? " is" : "s are"} waiting for you in Hires` : "No hiring decisions waiting on you"}`,
|
|
@@ -3300,34 +3797,154 @@ Acceptance criteria:
|
|
|
3300
3797
|
}
|
|
3301
3798
|
};
|
|
3302
3799
|
}
|
|
3800
|
+
function demoChime(chars) {
|
|
3801
|
+
const rate = 8e3;
|
|
3802
|
+
const seconds = Math.min(0.4 + chars / 400, 2);
|
|
3803
|
+
const n = Math.round(rate * seconds);
|
|
3804
|
+
const wav = Buffer.alloc(44 + n);
|
|
3805
|
+
wav.write("RIFF", 0);
|
|
3806
|
+
wav.writeUInt32LE(36 + n, 4);
|
|
3807
|
+
wav.write("WAVEfmt ", 8);
|
|
3808
|
+
wav.writeUInt32LE(16, 16);
|
|
3809
|
+
wav.writeUInt16LE(1, 20);
|
|
3810
|
+
wav.writeUInt16LE(1, 22);
|
|
3811
|
+
wav.writeUInt32LE(rate, 24);
|
|
3812
|
+
wav.writeUInt32LE(rate, 28);
|
|
3813
|
+
wav.writeUInt16LE(1, 32);
|
|
3814
|
+
wav.writeUInt16LE(8, 34);
|
|
3815
|
+
wav.write("data", 36);
|
|
3816
|
+
wav.writeUInt32LE(n, 40);
|
|
3817
|
+
for (let i = 0; i < n; i++) {
|
|
3818
|
+
const t = i / rate;
|
|
3819
|
+
const f = t < seconds / 2 ? 660 : 880;
|
|
3820
|
+
const fade = Math.min(1, (seconds - t) * 8, t * 40);
|
|
3821
|
+
wav[44 + i] = Math.round(128 + 40 * fade * Math.sin(2 * Math.PI * f * t));
|
|
3822
|
+
}
|
|
3823
|
+
return wav;
|
|
3824
|
+
}
|
|
3825
|
+
var demoVoices = [
|
|
3826
|
+
{ id: "demoVoiceAvery00001", name: "Avery", category: "premade", labels: { accent: "american", gender: "female", age: "middle aged", description: "calm", use_case: "conversational" }, previewUrl: null },
|
|
3827
|
+
{ id: "demoVoiceBasil00002", name: "Basil", category: "premade", labels: { accent: "british", gender: "male", age: "middle aged", description: "warm", use_case: "narration" }, previewUrl: null },
|
|
3828
|
+
{ id: "demoVoiceCleo000003", name: "Cleo", category: "premade", labels: { accent: "australian", gender: "female", age: "young", description: "friendly", use_case: "conversational" }, previewUrl: null }
|
|
3829
|
+
];
|
|
3830
|
+
var demoVoice = {
|
|
3831
|
+
checkKey: async (key) => {
|
|
3832
|
+
if (/bad/i.test(key)) throw new VoiceApiError(401, "401: invalid_api_key (demo)");
|
|
3833
|
+
},
|
|
3834
|
+
listVoices: async () => demoVoices,
|
|
3835
|
+
synthesize: async (_key, { text }) => {
|
|
3836
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3837
|
+
return demoChime(text.length);
|
|
3838
|
+
}
|
|
3839
|
+
};
|
|
3840
|
+
|
|
3841
|
+
// server/screenReply.ts
|
|
3842
|
+
function screenStatus(shot) {
|
|
3843
|
+
if (shot === void 0) return 404;
|
|
3844
|
+
return shot ? 200 : 204;
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
// server/sendBack.ts
|
|
3848
|
+
import { z } from "zod";
|
|
3849
|
+
|
|
3850
|
+
// server/mergeGate.ts
|
|
3851
|
+
var MAX_MERGE_FIXES = 3;
|
|
3852
|
+
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
3853
|
+
var MERGE_RETRY_MS = 10 * 6e4;
|
|
3854
|
+
function mergeStep(pr, rec, now2, opts) {
|
|
3855
|
+
if (pr.isDraft) return { do: "wait", note: "a draft: waiting", set: {} };
|
|
3856
|
+
if (!opts.detailed && (pr.mergeable === "UNKNOWN" || pr.mergeState === "UNKNOWN")) return { do: "details", set: {} };
|
|
3857
|
+
const set = {};
|
|
3858
|
+
if (rec.passedSha == null) set.passedSha = pr.headSha;
|
|
3859
|
+
if (pr.headSha !== (set.passedSha ?? rec.passedSha)) return { do: "requeue", set };
|
|
3860
|
+
const needsHuman = rec.mergeFixes >= MAX_MERGE_FIXES;
|
|
3861
|
+
if (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY") {
|
|
3862
|
+
return { do: "send-back", reason: "conflict", instructions: `It conflicts with ${opts.base}.`, needsHuman, set };
|
|
3863
|
+
}
|
|
3864
|
+
if (pr.checks === "failing") {
|
|
3865
|
+
const instructions = pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n");
|
|
3866
|
+
return { do: "send-back", reason: "checks", instructions, needsHuman, set };
|
|
3867
|
+
}
|
|
3868
|
+
if (pr.checks === "pending") {
|
|
3869
|
+
const since = rec.pendingSince ?? now2;
|
|
3870
|
+
if (rec.pendingSince == null) set.pendingSince = since;
|
|
3871
|
+
const alert = now2 - since > CHECKS_ALERT_MS && !rec.alerted;
|
|
3872
|
+
if (alert) set.alerted = true;
|
|
3873
|
+
return { do: "wait", note: `waiting for checks: ${pr.pendingChecks.join(", ") || "running"}`, alert, set };
|
|
3874
|
+
}
|
|
3875
|
+
set.pendingSince = null;
|
|
3876
|
+
if (pr.mergeable === "UNKNOWN") return { do: "wait", note: "GitHub is checking it can merge", set };
|
|
3877
|
+
if (pr.mergeState === "BEHIND") return { do: "update-branch", set };
|
|
3878
|
+
if (rec.mergeRetryAt && now2 < rec.mergeRetryAt) return { do: "wait", set };
|
|
3879
|
+
return { do: "merge", set };
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// server/qaOutcome.ts
|
|
3883
|
+
var MAX_QA_ROUNDS = 3;
|
|
3884
|
+
function lastQaRound(rec) {
|
|
3885
|
+
return rec.round - rec.retests >= MAX_QA_ROUNDS;
|
|
3886
|
+
}
|
|
3887
|
+
function qaOutcome(pass, rec, pr) {
|
|
3888
|
+
if (pass) return "passed";
|
|
3889
|
+
if (!lastQaRound(rec)) return "failed";
|
|
3890
|
+
const conflicting = pr != null && (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY");
|
|
3891
|
+
return conflicting && rec.mergeFixes < MAX_MERGE_FIXES ? "conflict" : "needs-human";
|
|
3892
|
+
}
|
|
3893
|
+
function conflictFixInstructions(qaInstructions2, base) {
|
|
3894
|
+
const merge = `It also conflicts with ${base}: bring it up to date (git fetch origin && git merge origin/${base}) and resolve the conflicts so both this change and the newly merged work keep working.`;
|
|
3895
|
+
return qaInstructions2.trim() ? `${qaInstructions2.trim()}
|
|
3896
|
+
|
|
3897
|
+
${merge}` : merge;
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// server/sendBack.ts
|
|
3901
|
+
var MAX_SEND_BACK_NOTE = 1e3;
|
|
3902
|
+
var body = z.object({ note: z.string().max(MAX_SEND_BACK_NOTE).optional() });
|
|
3903
|
+
function parseSendBackNote(input) {
|
|
3904
|
+
const parsed = body.safeParse(input ?? {});
|
|
3905
|
+
if (!parsed.success) throw new HttpError(400, `note must be text of at most ${MAX_SEND_BACK_NOTE} characters`);
|
|
3906
|
+
return parsed.data.note?.trim() || void 0;
|
|
3907
|
+
}
|
|
3908
|
+
function sendBackPatch(prNumber, pr, rec, base, note) {
|
|
3909
|
+
if (!pr || pr.state !== "OPEN") throw new HttpError(404, `PR #${prNumber} is not open`);
|
|
3910
|
+
if (rec?.status === "testing" || rec?.status === "fixing") throw new HttpError(409, `PR #${prNumber} is already ${rec.status}`);
|
|
3911
|
+
if (!rec || rec.status !== "needs-human" && rec.status !== "failed") {
|
|
3912
|
+
throw new HttpError(409, `PR #${prNumber} isn't waiting on you: only a PR that failed QA or needs a human can go back to a developer`);
|
|
3913
|
+
}
|
|
3914
|
+
const conflict = pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY";
|
|
3915
|
+
const findings = [rec.fixInstructions?.trim(), note?.trim() && `From the manager: ${note.trim()}`].filter(Boolean).join("\n\n");
|
|
3916
|
+
const addMerge = conflict && !rec.passedSha && !findings.includes(`conflicts with ${base}`);
|
|
3917
|
+
const fixInstructions = (addMerge ? conflictFixInstructions(findings, base) : findings) || null;
|
|
3918
|
+
return { status: "failed", fixReason: conflict ? "conflict" : "qa", fixInstructions, sessionFailures: 0, retests: rec.retests + 1, mergeNote: null };
|
|
3919
|
+
}
|
|
3303
3920
|
|
|
3304
3921
|
// server/swarm.ts
|
|
3305
|
-
import
|
|
3306
|
-
import
|
|
3307
|
-
import
|
|
3922
|
+
import fs13 from "node:fs/promises";
|
|
3923
|
+
import path15 from "node:path";
|
|
3924
|
+
import crypto7 from "node:crypto";
|
|
3308
3925
|
|
|
3309
3926
|
// server/ceo.ts
|
|
3310
3927
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
3311
|
-
import { z } from "zod";
|
|
3928
|
+
import { z as z2 } from "zod";
|
|
3312
3929
|
|
|
3313
3930
|
// shared/issues.ts
|
|
3314
3931
|
function issueSpecialty(labels) {
|
|
3315
3932
|
const l = labels.find((x) => /^swarm:/i.test(x) && !/^swarm:skip$/i.test(x));
|
|
3316
3933
|
return l ? l.slice(6).toLowerCase() : "";
|
|
3317
3934
|
}
|
|
3318
|
-
function blockers(
|
|
3935
|
+
function blockers(body2, open) {
|
|
3319
3936
|
const out = /* @__PURE__ */ new Set();
|
|
3320
|
-
for (const m of (
|
|
3937
|
+
for (const m of (body2 ?? "").matchAll(/\b(?:depends\s+on|blocked\s+by)\s*:?\s*((?:#\d+(?:\s*(?:,|and|&)\s*)?)+)/gi)) {
|
|
3321
3938
|
for (const n of m[1].matchAll(/#(\d+)/g)) if (open.has(Number(n[1]))) out.add(Number(n[1]));
|
|
3322
3939
|
}
|
|
3323
3940
|
return [...out];
|
|
3324
3941
|
}
|
|
3325
3942
|
var DEPENDENCY = /\b(?:depends\s+on|blocked\s+by)\s*:?\s*(?:#\d+(?:\s*(?:,|and|&)\s*)?)+/gi;
|
|
3326
3943
|
var STATEMENT = new RegExp(`\\s*${DEPENDENCY.source}[.;,]?`, "gi");
|
|
3327
|
-
function setDependsOn(
|
|
3944
|
+
function setDependsOn(body2, deps) {
|
|
3328
3945
|
const lines = [];
|
|
3329
3946
|
let dropped = false;
|
|
3330
|
-
for (const line of (
|
|
3947
|
+
for (const line of (body2 ?? "").split(/\r?\n/)) {
|
|
3331
3948
|
const rest2 = line.match(DEPENDENCY) ? line.replace(STATEMENT, "").trimEnd() : line;
|
|
3332
3949
|
if (rest2 !== line && !/[\p{L}\p{N}]/u.test(rest2)) {
|
|
3333
3950
|
dropped = true;
|
|
@@ -3376,7 +3993,7 @@ function holdUps(issues) {
|
|
|
3376
3993
|
}
|
|
3377
3994
|
|
|
3378
3995
|
// server/ceo.ts
|
|
3379
|
-
var EFFORT =
|
|
3996
|
+
var EFFORT = z2.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
3380
3997
|
function createOfficeTools(h) {
|
|
3381
3998
|
const run2 = async (fn) => {
|
|
3382
3999
|
try {
|
|
@@ -3395,21 +4012,21 @@ function createOfficeTools(h) {
|
|
|
3395
4012
|
tool(
|
|
3396
4013
|
"agent_detail",
|
|
3397
4014
|
"One agent in full: title, specialty, role, status, current task, model and effort, and their complete job description (company_status shortens long ones). Read it before rewriting someone's job description.",
|
|
3398
|
-
{ agent_id:
|
|
4015
|
+
{ agent_id: z2.string().describe("An id (or name) from company_status") },
|
|
3399
4016
|
(a) => run2(() => h.agentDetail(a))
|
|
3400
4017
|
),
|
|
3401
4018
|
tool(
|
|
3402
4019
|
"set_floor_profile",
|
|
3403
4020
|
"Record your read of a floor's project: a one-line summary (kind of project and stack), the QA brief that tells QA testers what to check for this kind of project, and how to run the app for the floor's preview monitor.",
|
|
3404
4021
|
{
|
|
3405
|
-
floor:
|
|
3406
|
-
summary:
|
|
3407
|
-
qa_brief:
|
|
3408
|
-
preview_command:
|
|
4022
|
+
floor: z2.number().int().describe("Floor number"),
|
|
4023
|
+
summary: z2.string().max(140).optional().describe('e.g. "3D browser game \xB7 Three.js + Vite + TypeScript"'),
|
|
4024
|
+
qa_brief: z2.string().max(2500).optional().describe("What QA must check on every PR for this project, as short bullet points"),
|
|
4025
|
+
preview_command: z2.string().max(2e3).optional().describe(
|
|
3409
4026
|
'Shell command that serves the app on port {port} from a fresh checkout after npm install, e.g. "npm run dev -- --port {port} --strictPort". PORT={port} is always set. {tmp} is a scratch folder. Empty string: back to the default (npm run dev, else start, else preview). Only set it when the default would not serve the app on PORT.'
|
|
3410
4027
|
),
|
|
3411
4028
|
// Not z.record(): the SDK can't turn it into JSON Schema, and one bad tool empties the whole tools/list.
|
|
3412
|
-
preview_env:
|
|
4029
|
+
preview_env: z2.object({}).catchall(z2.string()).optional().describe("Extra environment variables for the preview; {port} and {tmp} are replaced in the values. Replaces the whole set.")
|
|
3413
4030
|
},
|
|
3414
4031
|
(a) => run2(() => h.setFloorProfile(a))
|
|
3415
4032
|
),
|
|
@@ -3417,10 +4034,10 @@ function createOfficeTools(h) {
|
|
|
3417
4034
|
"update_job",
|
|
3418
4035
|
"Change an existing agent's job title, specialty or job description so it fits the project. Takes effect from their next task.",
|
|
3419
4036
|
{
|
|
3420
|
-
agent_id:
|
|
3421
|
-
title:
|
|
3422
|
-
specialty:
|
|
3423
|
-
job_description:
|
|
4037
|
+
agent_id: z2.string(),
|
|
4038
|
+
title: z2.string().max(60).optional(),
|
|
4039
|
+
specialty: z2.string().max(24).optional().describe('Short lowercase slug, e.g. "graphics"; "" for a generalist'),
|
|
4040
|
+
job_description: z2.string().max(2500).optional()
|
|
3424
4041
|
},
|
|
3425
4042
|
(a) => run2(() => h.updateJob(a))
|
|
3426
4043
|
),
|
|
@@ -3428,13 +4045,13 @@ function createOfficeTools(h) {
|
|
|
3428
4045
|
"propose_hire",
|
|
3429
4046
|
"Propose hiring a developer or QA tester for a floor. The manager approves or declines (or it is auto-approved if hiring is set to auto and the floor is under its team cap).",
|
|
3430
4047
|
{
|
|
3431
|
-
floor:
|
|
3432
|
-
role:
|
|
3433
|
-
title:
|
|
3434
|
-
specialty:
|
|
3435
|
-
job_description:
|
|
3436
|
-
reason:
|
|
3437
|
-
model:
|
|
4048
|
+
floor: z2.number().int(),
|
|
4049
|
+
role: z2.enum(["dev", "qa"]).describe("dev = builds issues into pull requests; qa = tests pull requests"),
|
|
4050
|
+
title: z2.string().max(60).describe('Specific job title, e.g. "Three.js graphics engineer"'),
|
|
4051
|
+
specialty: z2.string().max(24).describe('Short lowercase slug used to label issues swarm:<specialty>, e.g. "graphics"'),
|
|
4052
|
+
job_description: z2.string().max(2500).describe("What this person owns on this project and how they should work. Written to them, second person."),
|
|
4053
|
+
reason: z2.string().max(600).describe("Why the floor needs them now. The manager reads this."),
|
|
4054
|
+
model: z2.string().optional().describe("Leave out to use the default model"),
|
|
3438
4055
|
effort: EFFORT.optional().describe("Leave out to use the default effort")
|
|
3439
4056
|
},
|
|
3440
4057
|
(a) => run2(() => h.proposeHire(a))
|
|
@@ -3442,17 +4059,17 @@ function createOfficeTools(h) {
|
|
|
3442
4059
|
tool(
|
|
3443
4060
|
"propose_let_go",
|
|
3444
4061
|
"Propose letting an agent go (overstaffed floor, specialty no longer needed). The manager decides.",
|
|
3445
|
-
{ agent_id:
|
|
4062
|
+
{ agent_id: z2.string(), reason: z2.string().max(600) },
|
|
3446
4063
|
(a) => run2(() => h.proposeLetGo(a))
|
|
3447
4064
|
),
|
|
3448
4065
|
tool(
|
|
3449
4066
|
"file_issue",
|
|
3450
4067
|
'File a GitHub issue on a floor. A specialty routes it to that specialist first; when none is free, any free developer takes it. Write "Depends on #N" in the body only when it cannot start until #N is merged: the office will not start it until #N is closed.',
|
|
3451
4068
|
{
|
|
3452
|
-
floor:
|
|
3453
|
-
title:
|
|
3454
|
-
body:
|
|
3455
|
-
specialty:
|
|
4069
|
+
floor: z2.number().int(),
|
|
4070
|
+
title: z2.string().max(120),
|
|
4071
|
+
body: z2.string().max(6e3).describe("Context, what to build, acceptance criteria"),
|
|
4072
|
+
specialty: z2.string().max(24).optional()
|
|
3456
4073
|
},
|
|
3457
4074
|
(a) => run2(() => h.fileIssue(a))
|
|
3458
4075
|
),
|
|
@@ -3460,10 +4077,10 @@ function createOfficeTools(h) {
|
|
|
3460
4077
|
"route_issue",
|
|
3461
4078
|
'Fix the routing of an open issue instead of filing a duplicate: change its specialty, rewrite its "Depends on #N" line, or both. The rest of the body stays as it is.',
|
|
3462
4079
|
{
|
|
3463
|
-
floor:
|
|
3464
|
-
number:
|
|
3465
|
-
specialty:
|
|
3466
|
-
depends_on:
|
|
4080
|
+
floor: z2.number().int(),
|
|
4081
|
+
number: z2.number().int().positive().describe("The issue number"),
|
|
4082
|
+
specialty: z2.string().max(24).optional().describe('Sets swarm:<specialty> and removes any other; "" for none. Someone on the floor, or a pending proposal, must have it.'),
|
|
4083
|
+
depends_on: z2.array(z2.number().int().positive()).max(10).optional().describe("Issues it waits for; [] for none. Not for an issue in progress.")
|
|
3467
4084
|
},
|
|
3468
4085
|
(a) => run2(() => h.routeIssue(a))
|
|
3469
4086
|
)
|
|
@@ -3624,14 +4241,14 @@ function planRoute(r) {
|
|
|
3624
4241
|
if (d === r.number) throw new Error(`#${r.number} can't depend on itself.`);
|
|
3625
4242
|
if (!open.has(d)) throw new Error(r.closed(d) ? `#${d} is closed, so there's nothing to wait for.` : `There is no open issue #${d} on floor ${r.floor}.`);
|
|
3626
4243
|
}
|
|
3627
|
-
const
|
|
3628
|
-
const after = r.issues.map((i) => i.number === r.number ? { ...i, body } : i);
|
|
4244
|
+
const body2 = setDependsOn(issue2.body, deps);
|
|
4245
|
+
const after = r.issues.map((i) => i.number === r.number ? { ...i, body: body2 } : i);
|
|
3629
4246
|
const waits = new Map(after.map((i) => [i.number, blockers(i.body, open)]));
|
|
3630
4247
|
const loop = deps.find((d) => reaches(d, r.number, waits));
|
|
3631
4248
|
if (loop !== void 0) throw new Error(`#${loop} already waits for #${r.number}, directly or through other issues, so that would be a cycle.`);
|
|
3632
4249
|
const depth = waitsDepth(r.number, waits) + (holdUps(after).get(r.number)?.chain ?? 0);
|
|
3633
4250
|
if (depth > 2) throw new Error(`That makes a dependency chain ${depth} steps deep through #${r.number}. Keep chains to 2 steps at most: split the work so more of it can start side by side.`);
|
|
3634
|
-
if (
|
|
4251
|
+
if (body2 !== issue2.body) plan.body = body2;
|
|
3635
4252
|
done.push(deps.length ? `depends on ${deps.map((d) => `#${d}`).join(", ")}` : "no dependencies");
|
|
3636
4253
|
}
|
|
3637
4254
|
plan.summary = `#${r.number} on floor ${r.floor}: ${done.join(", ")}.`;
|
|
@@ -3643,12 +4260,19 @@ function reaches(from, to, waits, seen = /* @__PURE__ */ new Set()) {
|
|
|
3643
4260
|
seen.add(from);
|
|
3644
4261
|
return (waits.get(from) ?? []).some((n) => reaches(n, to, waits, seen));
|
|
3645
4262
|
}
|
|
3646
|
-
function specialtySlug(
|
|
3647
|
-
const slug = String(
|
|
4263
|
+
function specialtySlug(s2) {
|
|
4264
|
+
const slug = String(s2 ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
3648
4265
|
return slug === "skip" ? "" : slug;
|
|
3649
4266
|
}
|
|
3650
4267
|
var specialtyLabel = (slug) => `swarm:${slug}`;
|
|
3651
4268
|
|
|
4269
|
+
// server/deskSetup.ts
|
|
4270
|
+
async function setUpDesk(term, steps) {
|
|
4271
|
+
await term?.releaseIdle?.();
|
|
4272
|
+
await steps.release();
|
|
4273
|
+
return steps.prepare();
|
|
4274
|
+
}
|
|
4275
|
+
|
|
3652
4276
|
// server/fixOutcome.ts
|
|
3653
4277
|
var MAX_FIX_FAILURES = 2;
|
|
3654
4278
|
function fixOutcome(rec, head, minutes, counts = true) {
|
|
@@ -3663,21 +4287,12 @@ function fixOutcome(rec, head, minutes, counts = true) {
|
|
|
3663
4287
|
pushed: false
|
|
3664
4288
|
};
|
|
3665
4289
|
}
|
|
3666
|
-
if (rec.fixReason === "checks" || rec.fixReason === "conflict") {
|
|
4290
|
+
if (rec.fixReason === "checks" || rec.fixReason === "conflict" && rec.passedSha) {
|
|
3667
4291
|
return { set: { status: "passed", sessionFailures: 0, mergeNote: "waiting for fresh checks" }, log: { kind: "done", text: `\u2714 PR #${pr} fixed in ${minutes}m. Back in line to merge.` }, pushed: true };
|
|
3668
4292
|
}
|
|
3669
4293
|
return { set: { status: "queued", round: rec.round + 1, sessionFailures: 0 }, log: { kind: "done", text: `\u2714 Fix pushed for PR #${pr} in ${minutes}m. Back to QA.` }, pushed: true };
|
|
3670
4294
|
}
|
|
3671
4295
|
|
|
3672
|
-
// server/httpError.ts
|
|
3673
|
-
var HttpError = class extends Error {
|
|
3674
|
-
constructor(status, message) {
|
|
3675
|
-
super(message);
|
|
3676
|
-
this.status = status;
|
|
3677
|
-
}
|
|
3678
|
-
status;
|
|
3679
|
-
};
|
|
3680
|
-
|
|
3681
4296
|
// server/issueClaims.ts
|
|
3682
4297
|
var RECENT_MERGE_MS = 6 * 60 * 6e4;
|
|
3683
4298
|
function branchIssue(headRefName) {
|
|
@@ -3697,38 +4312,6 @@ function issueTaken(n, agents, pulls, now2) {
|
|
|
3697
4312
|
});
|
|
3698
4313
|
}
|
|
3699
4314
|
|
|
3700
|
-
// server/mergeGate.ts
|
|
3701
|
-
var MAX_MERGE_FIXES = 3;
|
|
3702
|
-
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
3703
|
-
var MERGE_RETRY_MS = 10 * 6e4;
|
|
3704
|
-
function mergeStep(pr, rec, now2, opts) {
|
|
3705
|
-
if (pr.isDraft) return { do: "wait", note: "a draft: waiting", set: {} };
|
|
3706
|
-
if (!opts.detailed && (pr.mergeable === "UNKNOWN" || pr.mergeState === "UNKNOWN")) return { do: "details", set: {} };
|
|
3707
|
-
const set = {};
|
|
3708
|
-
if (rec.passedSha == null) set.passedSha = pr.headSha;
|
|
3709
|
-
if (pr.headSha !== (set.passedSha ?? rec.passedSha)) return { do: "requeue", set };
|
|
3710
|
-
const needsHuman = rec.mergeFixes >= MAX_MERGE_FIXES;
|
|
3711
|
-
if (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY") {
|
|
3712
|
-
return { do: "send-back", reason: "conflict", instructions: `It conflicts with ${opts.base}.`, needsHuman, set };
|
|
3713
|
-
}
|
|
3714
|
-
if (pr.checks === "failing") {
|
|
3715
|
-
const instructions = pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n");
|
|
3716
|
-
return { do: "send-back", reason: "checks", instructions, needsHuman, set };
|
|
3717
|
-
}
|
|
3718
|
-
if (pr.checks === "pending") {
|
|
3719
|
-
const since = rec.pendingSince ?? now2;
|
|
3720
|
-
if (rec.pendingSince == null) set.pendingSince = since;
|
|
3721
|
-
const alert = now2 - since > CHECKS_ALERT_MS && !rec.alerted;
|
|
3722
|
-
if (alert) set.alerted = true;
|
|
3723
|
-
return { do: "wait", note: `waiting for checks: ${pr.pendingChecks.join(", ") || "running"}`, alert, set };
|
|
3724
|
-
}
|
|
3725
|
-
set.pendingSince = null;
|
|
3726
|
-
if (pr.mergeable === "UNKNOWN") return { do: "wait", note: "GitHub is checking it can merge", set };
|
|
3727
|
-
if (pr.mergeState === "BEHIND") return { do: "update-branch", set };
|
|
3728
|
-
if (rec.mergeRetryAt && now2 < rec.mergeRetryAt) return { do: "wait", set };
|
|
3729
|
-
return { do: "merge", set };
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
4315
|
// server/qaOrphans.ts
|
|
3733
4316
|
function orphanedQa(qa, agents, busy) {
|
|
3734
4317
|
return qa.filter(
|
|
@@ -3736,6 +4319,39 @@ function orphanedQa(qa, agents, busy) {
|
|
|
3736
4319
|
);
|
|
3737
4320
|
}
|
|
3738
4321
|
|
|
4322
|
+
// server/qaPrompt.ts
|
|
4323
|
+
function qaInstructions(i) {
|
|
4324
|
+
return { checks: checksLine(i), testStep: testStep(i.checks !== "none"), retest: retest(i) };
|
|
4325
|
+
}
|
|
4326
|
+
function checksLine(i) {
|
|
4327
|
+
const names = (list) => list.join(", ") || "unnamed";
|
|
4328
|
+
const state = i.checks === "passing" ? `passing (${names(i.checkNames)})` : i.checks === "failing" ? `failing: ${names(i.failedChecks)}${i.pendingChecks.length ? `; still running: ${i.pendingChecks.join(", ")}` : ""}` : i.checks === "pending" ? `pending: ${names(i.pendingChecks)}` : "none";
|
|
4329
|
+
return `GitHub checks right now: ${state}`;
|
|
4330
|
+
}
|
|
4331
|
+
function testStep(hasChecks) {
|
|
4332
|
+
return hasChecks ? "3. GitHub's checks run on this PR, and it merges only once they're green. Read .github/workflows to see what they cover and don't re-run that locally. Run only what you need to exercise the change (for example a build to start the app), anything the checks don't cover, or a failing check to reproduce it. A failing check is a finding: name it." : "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).";
|
|
4333
|
+
}
|
|
4334
|
+
function retest(i) {
|
|
4335
|
+
const since = i.lastTestedSha && i.lastTestedSha !== i.headSha ? i.lastTestedSha : null;
|
|
4336
|
+
const diff = since ? `(git diff ${since}..HEAD), plus a quick smoke test of the main flow. Don't repeat the full test plan for parts the change didn't touch.` : null;
|
|
4337
|
+
if (i.fixReason === "conflict") {
|
|
4338
|
+
return `
|
|
4339
|
+
QA passed it before, but since then the branch was updated with ${i.defaultBranch} to resolve merge conflicts. Re-check everything, especially where this change meets the newly merged work.`;
|
|
4340
|
+
}
|
|
4341
|
+
if (i.fixReason === "checks") {
|
|
4342
|
+
return `
|
|
4343
|
+
QA passed it before, but since then the developer changed the code to fix failing GitHub checks. ${diff ? `Test what changed since QA passed it ${diff}` : "Re-check everything."}`;
|
|
4344
|
+
}
|
|
4345
|
+
if (i.round > 1 && i.summary) {
|
|
4346
|
+
return `
|
|
4347
|
+
This is a re-test after fixes. Last round's findings:
|
|
4348
|
+
${i.summary}
|
|
4349
|
+
${i.fixInstructions ?? ""}
|
|
4350
|
+
Check those first, then ${diff ? `test what changed since the last round ${diff}` : "re-check everything else."}`;
|
|
4351
|
+
}
|
|
4352
|
+
return "";
|
|
4353
|
+
}
|
|
4354
|
+
|
|
3739
4355
|
// server/restartRecovery.ts
|
|
3740
4356
|
function followKeptCli(a, cli) {
|
|
3741
4357
|
if (!cli?.busy || a.status === "preparing") return false;
|
|
@@ -3865,9 +4481,9 @@ var Previews = class {
|
|
|
3865
4481
|
r.handle = this.backend.previews.start(
|
|
3866
4482
|
{ fullName: f.fullName, defaultBranch: f.defaultBranch, pr: pr ?? null, port, command: f.preview.command, env: { ...f.preview.env }, title: `${title} \xB7 ${ref}` },
|
|
3867
4483
|
{
|
|
3868
|
-
status: (
|
|
4484
|
+
status: (s2) => {
|
|
3869
4485
|
if (!live()) return;
|
|
3870
|
-
r.status =
|
|
4486
|
+
r.status = s2;
|
|
3871
4487
|
this.hooks.emit(f.id);
|
|
3872
4488
|
},
|
|
3873
4489
|
commit: (sha) => {
|
|
@@ -3969,7 +4585,7 @@ var PLACEHOLDERS = {
|
|
|
3969
4585
|
};
|
|
3970
4586
|
var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
|
|
3971
4587
|
var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
|
|
3972
|
-
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing }) {
|
|
4588
|
+
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing, depsLine }) {
|
|
3973
4589
|
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
3974
4590
|
const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
|
|
3975
4591
|
return [
|
|
@@ -3983,6 +4599,7 @@ ${a.brief}` : "",
|
|
|
3983
4599
|
repo.summary ? `Project: ${repo.summary}` : "",
|
|
3984
4600
|
repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
|
|
3985
4601
|
`Your worktree: ${cwd}`,
|
|
4602
|
+
depsLine ?? "",
|
|
3986
4603
|
fixing ? `You are fixing pull request #${fixing.pr}. Its code is checked out on local branch ${branch}; push fixes with: ${push}. Do not open a new pull request.` : `Your branch: ${branch} (already checked out, created from origin/${repo.defaultBranch})`,
|
|
3987
4604
|
links.length ? `Related repositories you may read for context (do not modify them):
|
|
3988
4605
|
${links.join("\n")}` : "",
|
|
@@ -3999,7 +4616,7 @@ ${links.join("\n")}` : "",
|
|
|
3999
4616
|
"Rules: never push to the default branch, never force-push, never merge pull requests yourself (the office merges them once QA and the checks pass), and never edit files outside your worktree. If you cannot finish, open a draft PR (gh pr create --draft) explaining what is left and why."
|
|
4000
4617
|
].filter((l) => l !== "").join("\n");
|
|
4001
4618
|
}
|
|
4002
|
-
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr }) {
|
|
4619
|
+
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, depsLine, testStep: testStep2 }) {
|
|
4003
4620
|
return [
|
|
4004
4621
|
`You are ${a.name}, ${a.title ? `the team's ${a.title},` : "a QA engineer"} on an autonomous agent team ("cubefarm"). Developers open pull requests; you review and independently verify each one before it is merged. Your sign-off is the review: ${repo.autoMerge ? "on this floor a PR you pass merges by itself as soon as GitHub's checks are green, so nobody else reads the code after you. " : ""}Be thorough and skeptical, but fair: fail a PR only for real problems (broken behaviour, failing tests or build, the issue's requirements not met, obvious regressions), not for style preferences.`,
|
|
4005
4622
|
...a.brief ? ["", `Your job description:
|
|
@@ -4013,13 +4630,15 @@ ${a.brief}`] : [],
|
|
|
4013
4630
|
${repo.qaBrief}`] : [],
|
|
4014
4631
|
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
4015
4632
|
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
4633
|
+
...depsLine ? [depsLine] : [],
|
|
4016
4634
|
"",
|
|
4017
4635
|
"How to test:",
|
|
4018
4636
|
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
4019
4637
|
`2. Review the code as a careful reviewer would: git diff origin/${repo.defaultBranch}...HEAD. Look for bugs, unhandled errors and edge cases, security problems, leftover debug code, and new logic without tests.`,
|
|
4020
|
-
"3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4638
|
+
testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4021
4639
|
repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${port} (reserved for you) and exercise the change in a real browser with the Playwright tools: navigate, click, type, resize to a phone size, try edge cases, and check the console for errors. Take a screenshot with browser_take_screenshot (no filename) of every important state: the screenshots are attached to the PR as evidence. Stop the server afterwards.` : "4. Exercise the changed behaviour directly (run the program, call the API, write a quick script).",
|
|
4022
4640
|
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
4641
|
+
"A merge conflict with the default branch is not a fail: judge the change itself (on a throwaway merge if you need newer work) and pass it if it's good. The office sends conflicting PRs back for a merge fix, then you re-test.",
|
|
4023
4642
|
"",
|
|
4024
4643
|
"Rules: do not modify the code under test, do not commit, push, comment on, review or merge anything on GitHub. The office posts your report on the pull request. Finish with the structured QA report: verdict, summary, the checks you performed, the commands you ran and one caption per screenshot."
|
|
4025
4644
|
].join("\n");
|
|
@@ -4085,8 +4704,8 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
4085
4704
|
}
|
|
4086
4705
|
|
|
4087
4706
|
// server/terminal.ts
|
|
4088
|
-
import
|
|
4089
|
-
import
|
|
4707
|
+
import fs12 from "node:fs/promises";
|
|
4708
|
+
import path14 from "node:path";
|
|
4090
4709
|
import xtermHeadless from "@xterm/headless";
|
|
4091
4710
|
import xtermSerialize from "@xterm/addon-serialize";
|
|
4092
4711
|
var { Terminal } = xtermHeadless;
|
|
@@ -4105,7 +4724,10 @@ var AgentTerminal = class {
|
|
|
4105
4724
|
rows = TERM_ROWS;
|
|
4106
4725
|
/** Output arrived since the last save. */
|
|
4107
4726
|
dirty = false;
|
|
4108
|
-
/**
|
|
4727
|
+
/**
|
|
4728
|
+
* Closes a CLI left waiting at its prompt after its task, settling once it has exited (set by the runtime that left
|
|
4729
|
+
* it there; null while a CLI drives a session).
|
|
4730
|
+
*/
|
|
4109
4731
|
releaseIdle = null;
|
|
4110
4732
|
/** Something the manager typed at that waiting prompt: the office takes it on as a follow-up (true) or refuses. */
|
|
4111
4733
|
onIdlePrompt = null;
|
|
@@ -4180,12 +4802,12 @@ var AgentTerminal = class {
|
|
|
4180
4802
|
async save(file) {
|
|
4181
4803
|
this.dirty = false;
|
|
4182
4804
|
await this.flush();
|
|
4183
|
-
await
|
|
4184
|
-
await
|
|
4185
|
-
await
|
|
4805
|
+
await fs12.mkdir(path14.dirname(file), { recursive: true });
|
|
4806
|
+
await fs12.writeFile(`${file}.tmp`, this.snapshot());
|
|
4807
|
+
await fs12.rename(`${file}.tmp`, file);
|
|
4186
4808
|
}
|
|
4187
4809
|
async load(file) {
|
|
4188
|
-
const data = await
|
|
4810
|
+
const data = await fs12.readFile(file, "utf8").catch(() => "");
|
|
4189
4811
|
if (!data) return;
|
|
4190
4812
|
this.term.write(data);
|
|
4191
4813
|
this.note("(the office restarted)");
|
|
@@ -4277,6 +4899,7 @@ function effectiveModel(own, cli, office, claudeDefault) {
|
|
|
4277
4899
|
|
|
4278
4900
|
// shared/types.ts
|
|
4279
4901
|
var CEO_ID = "ceo";
|
|
4902
|
+
var INSTALL_STEP = "Installing dependencies";
|
|
4280
4903
|
|
|
4281
4904
|
// server/swarm.ts
|
|
4282
4905
|
var FLOOR_COLORS = ["#ff8a5b", "#4fb3e8", "#8fd14f", "#c77dff", "#ffc93c", "#ff6fb5", "#2ec4b6", "#f25f5c"];
|
|
@@ -4316,33 +4939,32 @@ var FEMININE_NAMES = new Set(
|
|
|
4316
4939
|
var lookFor = (name) => FEMININE_NAMES.has(name.trim().split(/\s+/)[0].toLowerCase()) ? "feminine" : "masculine";
|
|
4317
4940
|
var LOOKS = ["feminine", "masculine"];
|
|
4318
4941
|
var MAX_DESKS = { ...FLOOR_DESKS, ceo: 1 };
|
|
4319
|
-
var MAX_QA_ROUNDS = 3;
|
|
4320
4942
|
var DEFAULT_MODEL = "claude-opus-5-5";
|
|
4321
4943
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
4322
4944
|
var CEO_MODEL = "claude-opus-5-5";
|
|
4323
4945
|
var CEO_EFFORT = "xhigh";
|
|
4324
4946
|
var CEO_NAME = "Morgan";
|
|
4325
|
-
var CEO_DIR =
|
|
4326
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
4947
|
+
var CEO_DIR = path15.join(HOME_DIR, "ceo");
|
|
4948
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path15.resolve(import.meta.dirname, ".."));
|
|
4327
4949
|
var MAX_ISSUES_PER_JOB = 12;
|
|
4328
4950
|
var KEEP_MESSAGES = 200;
|
|
4329
4951
|
var KEEP_DECIDED_REQUESTS = 40;
|
|
4330
4952
|
var pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
4331
|
-
var slugify = (
|
|
4953
|
+
var slugify = (s2) => s2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "agent";
|
|
4332
4954
|
var BUSY = ["preparing", "working"];
|
|
4333
4955
|
var FREE = ["idle", "done"];
|
|
4334
4956
|
var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
4335
4957
|
var MAX_ISSUE_FAILURES = 2;
|
|
4336
4958
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
4337
4959
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
4338
|
-
var SCREENS_DIR =
|
|
4960
|
+
var SCREENS_DIR = path15.join(HOME_DIR, "screens");
|
|
4339
4961
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
4340
|
-
var screenFile = (agentId, mime) =>
|
|
4962
|
+
var screenFile = (agentId, mime) => path15.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
4341
4963
|
async function loadScreen(agentId) {
|
|
4342
4964
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
4343
|
-
const file =
|
|
4965
|
+
const file = path15.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
4344
4966
|
try {
|
|
4345
|
-
const [data, stat] = await Promise.all([
|
|
4967
|
+
const [data, stat] = await Promise.all([fs13.readFile(file), fs13.stat(file)]);
|
|
4346
4968
|
return { data, mime, at: stat.mtimeMs };
|
|
4347
4969
|
} catch {
|
|
4348
4970
|
}
|
|
@@ -4350,10 +4972,10 @@ async function loadScreen(agentId) {
|
|
|
4350
4972
|
return null;
|
|
4351
4973
|
}
|
|
4352
4974
|
async function removeScreens(agentId) {
|
|
4353
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
4975
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs13.rm(path15.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
4354
4976
|
}
|
|
4355
|
-
var TERMINALS_DIR =
|
|
4356
|
-
var terminalFile = (agentId) =>
|
|
4977
|
+
var TERMINALS_DIR = path15.join(HOME_DIR, "terminals");
|
|
4978
|
+
var terminalFile = (agentId) => path15.join(TERMINALS_DIR, `${agentId}.ansi`);
|
|
4357
4979
|
var TERMINAL_SAVE_MS = 2e4;
|
|
4358
4980
|
var QA_SCHEMA = {
|
|
4359
4981
|
type: "object",
|
|
@@ -4419,12 +5041,20 @@ function parseReport(result) {
|
|
|
4419
5041
|
fixInstructions: r.fixInstructions ? String(r.fixInstructions) : void 0
|
|
4420
5042
|
};
|
|
4421
5043
|
}
|
|
4422
|
-
var cell = (
|
|
5044
|
+
var cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
4423
5045
|
var ICON = { pass: "\u2705", fail: "\u274C", skip: "\u23ED\uFE0F" };
|
|
4424
5046
|
var Swarm = class {
|
|
4425
|
-
// coding-agent CLIs found on this machine (detected at startup)
|
|
4426
5047
|
constructor(backend) {
|
|
4427
5048
|
this.backend = backend;
|
|
5049
|
+
this.voice = new Voice({
|
|
5050
|
+
api: backend.voice,
|
|
5051
|
+
secretsFile: path15.join(HOME_DIR, backend.demo ? "demo-secrets.json" : "secrets.json"),
|
|
5052
|
+
cacheDir: path15.join(HOME_DIR, backend.demo ? "demo-voice" : "voice"),
|
|
5053
|
+
settings: () => this.state.settings.voice,
|
|
5054
|
+
message: (id) => this.state.messages.find((m) => m.id === id),
|
|
5055
|
+
officeNote: (text) => this.postMessage("office", text),
|
|
5056
|
+
keyChanged: (view2) => this.broadcast({ type: "voiceKey", ...view2 })
|
|
5057
|
+
});
|
|
4428
5058
|
this.previews = new Previews(backend, {
|
|
4429
5059
|
emit: (id) => {
|
|
4430
5060
|
const r = this.state.repos.find((x) => x.id === id);
|
|
@@ -4450,7 +5080,8 @@ var Swarm = class {
|
|
|
4450
5080
|
setupDone: false,
|
|
4451
5081
|
tutorialStep: 0,
|
|
4452
5082
|
autoUpdate: true,
|
|
4453
|
-
pacingSessions: DEFAULT_PACING_SESSIONS
|
|
5083
|
+
pacingSessions: DEFAULT_PACING_SESSIONS,
|
|
5084
|
+
voice: { ...DEFAULT_VOICE }
|
|
4454
5085
|
},
|
|
4455
5086
|
repos: [],
|
|
4456
5087
|
agents: [],
|
|
@@ -4481,6 +5112,8 @@ var Swarm = class {
|
|
|
4481
5112
|
messageSeq = 1;
|
|
4482
5113
|
agentRt = /* @__PURE__ */ new Map();
|
|
4483
5114
|
repoRt = /* @__PURE__ */ new Map();
|
|
5115
|
+
deskAlerts = /* @__PURE__ */ new Map();
|
|
5116
|
+
// agent id -> the desk setup error the manager was last told about
|
|
4484
5117
|
clients = /* @__PURE__ */ new Set();
|
|
4485
5118
|
user = null;
|
|
4486
5119
|
ghError;
|
|
@@ -4504,10 +5137,13 @@ var Swarm = class {
|
|
|
4504
5137
|
};
|
|
4505
5138
|
lastOfficeView = "";
|
|
4506
5139
|
clis = [];
|
|
5140
|
+
// coding-agent CLIs found on this machine (detected at startup)
|
|
5141
|
+
/** Phone messages read aloud. The demo keeps its own key and clips, so it never touches the real ones. */
|
|
5142
|
+
voice;
|
|
4507
5143
|
// ---------- lifecycle ----------
|
|
4508
5144
|
async init() {
|
|
4509
5145
|
try {
|
|
4510
|
-
const raw = await
|
|
5146
|
+
const raw = await fs13.readFile(STATE_FILE, "utf8");
|
|
4511
5147
|
const loaded = JSON.parse(raw);
|
|
4512
5148
|
this.state = {
|
|
4513
5149
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -4562,11 +5198,13 @@ var Swarm = class {
|
|
|
4562
5198
|
delete old.maxConcurrent;
|
|
4563
5199
|
}
|
|
4564
5200
|
delete old.permissionMode;
|
|
5201
|
+
this.state.settings.voice = voiceSettings(DEFAULT_VOICE, loaded.settings?.voice);
|
|
4565
5202
|
if (loaded.settings && loaded.settings.setupDone === void 0 && this.state.repos.length > 0) {
|
|
4566
5203
|
Object.assign(this.state.settings, { setupDone: true, tutorialStep: -1 });
|
|
4567
5204
|
}
|
|
4568
5205
|
} catch {
|
|
4569
5206
|
}
|
|
5207
|
+
await this.voice.init();
|
|
4570
5208
|
for (const r of this.state.repos) if (r.localPath) this.backend.setLocalPath(r.fullName, r.localPath);
|
|
4571
5209
|
const interrupted = [];
|
|
4572
5210
|
for (const a of this.state.agents) {
|
|
@@ -4762,12 +5400,15 @@ var Swarm = class {
|
|
|
4762
5400
|
phoneReadAt: this.state.phoneReadAt,
|
|
4763
5401
|
usage: this.usageNow(),
|
|
4764
5402
|
clis: this.clis,
|
|
5403
|
+
...this.voice.keyView(),
|
|
4765
5404
|
officeCommit: this.officeHead?.slice(0, 7) ?? null,
|
|
4766
5405
|
officeUpdate: this.officeHead ? this.officeUpdateView() : void 0
|
|
4767
5406
|
};
|
|
4768
5407
|
}
|
|
5408
|
+
/** The agent's latest screenshot: null when it has none right now, undefined for an unknown agent. */
|
|
4769
5409
|
screenshot(agentId) {
|
|
4770
|
-
|
|
5410
|
+
const rt = this.agentRt.get(agentId);
|
|
5411
|
+
return rt ? rt.screenshot : void 0;
|
|
4771
5412
|
}
|
|
4772
5413
|
// ---------- clients ----------
|
|
4773
5414
|
addClient(ws) {
|
|
@@ -4829,10 +5470,10 @@ var Swarm = class {
|
|
|
4829
5470
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
4830
5471
|
this.saveTimer = null;
|
|
4831
5472
|
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
4832
|
-
await
|
|
5473
|
+
await fs13.mkdir(path15.dirname(STATE_FILE), { recursive: true });
|
|
4833
5474
|
const tmp = `${STATE_FILE}.tmp`;
|
|
4834
|
-
await
|
|
4835
|
-
await
|
|
5475
|
+
await fs13.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
5476
|
+
await fs13.rename(tmp, STATE_FILE);
|
|
4836
5477
|
}
|
|
4837
5478
|
// ---------- lookups ----------
|
|
4838
5479
|
repo(id) {
|
|
@@ -4914,7 +5555,7 @@ var Swarm = class {
|
|
|
4914
5555
|
const name = fullName.split("/")[1];
|
|
4915
5556
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
4916
5557
|
if (taken) throw new HttpError(409, `${taken.path} already exists and isn't a clone of ${fullName}. Connect that folder instead, or rename it first.`);
|
|
4917
|
-
return
|
|
5558
|
+
return path15.join(root, name);
|
|
4918
5559
|
}
|
|
4919
5560
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
4920
5561
|
async createRepo(name, opts) {
|
|
@@ -4926,11 +5567,11 @@ var Swarm = class {
|
|
|
4926
5567
|
}
|
|
4927
5568
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
4928
5569
|
async listProjectFolders(dir) {
|
|
4929
|
-
const root = dir?.trim() ?
|
|
5570
|
+
const root = dir?.trim() ? path15.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
4930
5571
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
4931
5572
|
throw new HttpError(400, err.message);
|
|
4932
5573
|
});
|
|
4933
|
-
const same = (a, b) =>
|
|
5574
|
+
const same = (a, b) => path15.resolve(a).toLowerCase() === path15.resolve(b).toLowerCase();
|
|
4934
5575
|
const floorOf = (f) => this.state.repos.find((r) => r.localPath && same(r.localPath, f.path) || f.github && r.id.toLowerCase() === f.github.toLowerCase())?.floor ?? null;
|
|
4935
5576
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
4936
5577
|
}
|
|
@@ -5108,13 +5749,13 @@ var Swarm = class {
|
|
|
5108
5749
|
return false;
|
|
5109
5750
|
}
|
|
5110
5751
|
/** QA passed, but the PR can't merge as it is: a developer fixes it, and QA re-tests if the code changed. */
|
|
5111
|
-
sendBack(repo, rec,
|
|
5752
|
+
sendBack(repo, rec, reason2, fixInstructions, needsHuman) {
|
|
5112
5753
|
if (needsHuman) {
|
|
5113
5754
|
this.setQa(rec, { status: "needs-human", mergeNote: null });
|
|
5114
|
-
this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} still ${
|
|
5755
|
+
this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} still ${reason2 === "conflict" ? `conflicts with ${repo.defaultBranch}` : "fails its checks"} after ${MAX_MERGE_FIXES} fixes, so it needs you.`);
|
|
5115
5756
|
return false;
|
|
5116
5757
|
}
|
|
5117
|
-
this.setQa(rec, { status: "failed", fixReason:
|
|
5758
|
+
this.setQa(rec, { status: "failed", fixReason: reason2, fixInstructions, mergeFixes: rec.mergeFixes + 1, mergeNote: null, pendingSince: null });
|
|
5118
5759
|
setTimeout(() => this.schedule(), 200);
|
|
5119
5760
|
return false;
|
|
5120
5761
|
}
|
|
@@ -5131,7 +5772,7 @@ var Swarm = class {
|
|
|
5131
5772
|
rt.folderSync = status;
|
|
5132
5773
|
this.emitRepo(repo);
|
|
5133
5774
|
if (status && status !== before) {
|
|
5134
|
-
const name = repo.localPath ?
|
|
5775
|
+
const name = repo.localPath ? path15.basename(repo.localPath) : repo.fullName;
|
|
5135
5776
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
5136
5777
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
5137
5778
|
}
|
|
@@ -5155,7 +5796,7 @@ var Swarm = class {
|
|
|
5155
5796
|
};
|
|
5156
5797
|
const r = await this.backend.sweepDesks(repo.fullName, keep);
|
|
5157
5798
|
const desks = r.desks + r.folders;
|
|
5158
|
-
const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${
|
|
5799
|
+
const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${path15.dirname(r.patches[0])})` : "";
|
|
5159
5800
|
console.log(`desk sweep ${repo.fullName}: ${desks} desks, ${r.branches} branches removed${saved}${r.skipped.length ? `, ${r.skipped.length} still in use` : ""}`);
|
|
5160
5801
|
if (!desks && !r.branches) return;
|
|
5161
5802
|
const removed = [desks && `${desks} old desk${desks === 1 ? "" : "s"}`, r.branches && `${r.branches} finished branch${r.branches === 1 ? "" : "es"}`].filter(Boolean).join(" and ");
|
|
@@ -5259,11 +5900,11 @@ var Swarm = class {
|
|
|
5259
5900
|
}
|
|
5260
5901
|
this.save();
|
|
5261
5902
|
}
|
|
5262
|
-
async createIssue(repoId2, title,
|
|
5903
|
+
async createIssue(repoId2, title, body2, assignTo, specialty) {
|
|
5263
5904
|
const repo = this.repo(repoId2);
|
|
5264
5905
|
if (!title.trim()) throw new HttpError(400, "An issue needs a title");
|
|
5265
5906
|
const slug = specialtySlug(specialty);
|
|
5266
|
-
const number = await this.backend.createIssue(repo.fullName, title.trim(),
|
|
5907
|
+
const number = await this.backend.createIssue(repo.fullName, title.trim(), body2, slug ? [specialtyLabel(slug)] : []);
|
|
5267
5908
|
await this.syncRepo(repo.id);
|
|
5268
5909
|
this.toast("success", `Issue #${number} filed on ${repo.fullName}`);
|
|
5269
5910
|
if (assignTo) await this.assign(assignTo, number);
|
|
@@ -5296,7 +5937,7 @@ var Swarm = class {
|
|
|
5296
5937
|
}
|
|
5297
5938
|
const name = opts.name?.trim() || this.freeName(role);
|
|
5298
5939
|
const agent = {
|
|
5299
|
-
id:
|
|
5940
|
+
id: crypto7.randomUUID(),
|
|
5300
5941
|
name,
|
|
5301
5942
|
repoId: repo.id,
|
|
5302
5943
|
role,
|
|
@@ -5383,7 +6024,7 @@ var Swarm = class {
|
|
|
5383
6024
|
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
5384
6025
|
this.agentRt.get(id)?.terminal?.dispose();
|
|
5385
6026
|
void removeScreens(id);
|
|
5386
|
-
void
|
|
6027
|
+
void fs13.rm(terminalFile(id), { force: true }).catch(() => void 0);
|
|
5387
6028
|
for (const q of this.state.qa) {
|
|
5388
6029
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
5389
6030
|
if (q.devAgentId === id) q.devAgentId = null;
|
|
@@ -5490,7 +6131,7 @@ var Swarm = class {
|
|
|
5490
6131
|
}
|
|
5491
6132
|
// ---------- terminals ----------
|
|
5492
6133
|
async loadTerminal(agentId) {
|
|
5493
|
-
if (!await
|
|
6134
|
+
if (!await fs13.stat(terminalFile(agentId)).catch(() => null)) return null;
|
|
5494
6135
|
const t = this.newTerminal(agentId);
|
|
5495
6136
|
await t.load(terminalFile(agentId));
|
|
5496
6137
|
return t;
|
|
@@ -5520,8 +6161,8 @@ var Swarm = class {
|
|
|
5520
6161
|
if (!t) return ws.close(4404, "That agent has no terminal");
|
|
5521
6162
|
t.attach(ws);
|
|
5522
6163
|
}
|
|
5523
|
-
buildSystemAppend(a, repo, cwd, branch, fixing) {
|
|
5524
|
-
return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing });
|
|
6164
|
+
buildSystemAppend(a, repo, cwd, branch, fixing, deps) {
|
|
6165
|
+
return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing, depsLine: depsPromptLine(deps) });
|
|
5525
6166
|
}
|
|
5526
6167
|
linkedDirs(repo) {
|
|
5527
6168
|
return this.linkedRepos(repo).map((r) => ({ fullName: r.fullName, dir: this.backend.mainDir(r.fullName) }));
|
|
@@ -5560,20 +6201,50 @@ var Swarm = class {
|
|
|
5560
6201
|
async prepare(a, repo, base, branch) {
|
|
5561
6202
|
try {
|
|
5562
6203
|
if (this.repoRt.get(repo.id)?.cloneStatus !== "ready") await this.cloneRepo(repo.id);
|
|
5563
|
-
|
|
5564
|
-
const
|
|
5565
|
-
|
|
6204
|
+
const slug = this.agentSlug(a);
|
|
6205
|
+
const note = (text) => this.appendLog(a, [{ kind: "system", text }]);
|
|
6206
|
+
const cwd = await setUpDesk(this.agentRt.get(a.id)?.terminal, {
|
|
6207
|
+
release: () => this.backend.releaseDesk(repo.fullName, slug, this.port(a)),
|
|
6208
|
+
prepare: () => this.backend.prepareDesk(repo.fullName, { defaultBranch: repo.defaultBranch, pr: base.pr }, slug, branch, note)
|
|
6209
|
+
});
|
|
6210
|
+
this.deskAlerts.delete(a.id);
|
|
6211
|
+
if (a.status !== "preparing") return null;
|
|
6212
|
+
const deps = await this.installDeps(a, cwd);
|
|
6213
|
+
return a.status === "preparing" ? { cwd, deps } : null;
|
|
5566
6214
|
} catch (err) {
|
|
5567
6215
|
if (a.status !== "preparing") return null;
|
|
5568
6216
|
a.status = "error";
|
|
5569
6217
|
a.endedAt = Date.now();
|
|
5570
6218
|
a.lastError = err.message;
|
|
5571
6219
|
this.appendLog(a, [{ kind: "error", text: `\u2717 ${a.lastError}` }]);
|
|
6220
|
+
if (this.deskAlerts.get(a.id) !== a.lastError) {
|
|
6221
|
+
this.deskAlerts.set(a.id, a.lastError);
|
|
6222
|
+
this.postMessage("office", `\u26A0\uFE0F ${a.name}'s desk couldn't be set up: ${a.lastError.slice(0, 240)}`);
|
|
6223
|
+
}
|
|
5572
6224
|
this.emitAgent(a);
|
|
5573
6225
|
this.save();
|
|
5574
6226
|
return null;
|
|
5575
6227
|
}
|
|
5576
6228
|
}
|
|
6229
|
+
/** The desk's dependencies, shown on the agent's card ("Installing dependencies") while npm runs. */
|
|
6230
|
+
async installDeps(a, cwd) {
|
|
6231
|
+
const rt = this.agentRt.get(a.id);
|
|
6232
|
+
try {
|
|
6233
|
+
return await this.backend.installDeps(cwd, {
|
|
6234
|
+
log: (lines) => this.appendLog(a, lines.map((text) => ({ kind: text.startsWith("\u26A0") ? "error" : "system", text }))),
|
|
6235
|
+
installing: () => {
|
|
6236
|
+
if (!rt) return;
|
|
6237
|
+
rt.currentTool = INSTALL_STEP;
|
|
6238
|
+
this.emitAgent(a);
|
|
6239
|
+
}
|
|
6240
|
+
});
|
|
6241
|
+
} finally {
|
|
6242
|
+
if (rt?.currentTool === INSTALL_STEP) {
|
|
6243
|
+
rt.currentTool = null;
|
|
6244
|
+
this.emitAgent(a);
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
5577
6248
|
async runTask(a, repo, issue2, note) {
|
|
5578
6249
|
const branch = devBranch(issue2.number, slugify(a.name));
|
|
5579
6250
|
this.beginTask(
|
|
@@ -5582,8 +6253,9 @@ var Swarm = class {
|
|
|
5582
6253
|
`Issue #${issue2.number}: ${issue2.title}`,
|
|
5583
6254
|
`Preparing worktree on ${branch}\u2026`
|
|
5584
6255
|
);
|
|
5585
|
-
const
|
|
5586
|
-
if (!
|
|
6256
|
+
const desk = await this.prepare(a, repo, {}, branch);
|
|
6257
|
+
if (!desk) return;
|
|
6258
|
+
const { cwd } = desk;
|
|
5587
6259
|
const prompt = [
|
|
5588
6260
|
`Please resolve GitHub issue #${issue2.number}: ${issue2.title}`,
|
|
5589
6261
|
`URL: ${issue2.url}`,
|
|
@@ -5593,7 +6265,7 @@ var Swarm = class {
|
|
|
5593
6265
|
note ? `
|
|
5594
6266
|
Note from the manager: ${note}` : ""
|
|
5595
6267
|
].filter(Boolean).join("\n");
|
|
5596
|
-
this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch));
|
|
6268
|
+
this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch, void 0, desk.deps));
|
|
5597
6269
|
}
|
|
5598
6270
|
startAgentSession(a, repo, cwd, prompt, systemAppend, resumeSessionId, outputSchema, mode = null) {
|
|
5599
6271
|
const rt = this.agentRt.get(a.id);
|
|
@@ -5640,7 +6312,7 @@ Note from the manager: ${note}` : ""
|
|
|
5640
6312
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
5641
6313
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
5642
6314
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
5643
|
-
void removeScreens(a.id).then(() =>
|
|
6315
|
+
void removeScreens(a.id).then(() => fs13.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs13.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
|
|
5644
6316
|
},
|
|
5645
6317
|
limited: (at) => this.pauseForLimit(at),
|
|
5646
6318
|
usageWarning: (info) => this.paceForWarning(info),
|
|
@@ -5776,8 +6448,22 @@ Note from the manager: ${note}` : ""
|
|
|
5776
6448
|
this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
|
|
5777
6449
|
this.toast("info", `PR #${prNumber} is queued for QA`);
|
|
5778
6450
|
}
|
|
5779
|
-
|
|
5780
|
-
|
|
6451
|
+
/** Manager's "send back to dev" for a PR that needs a human or failed QA: a developer fixes it, with the note. */
|
|
6452
|
+
async sendBackToDev(repoId2, prNumber, note) {
|
|
6453
|
+
const repo = this.repo(repoId2);
|
|
6454
|
+
const find = () => this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === prNumber);
|
|
6455
|
+
const listed = this.repoRt.get(repo.id)?.pulls.find((p) => p.number === prNumber);
|
|
6456
|
+
sendBackPatch(prNumber, listed, find(), repo.defaultBranch, note);
|
|
6457
|
+
const details = await this.backend.prDetails(repo.fullName, prNumber).catch(() => null);
|
|
6458
|
+
const pr = details && listed ? { ...listed, state: details.state, mergeable: details.mergeable, mergeState: details.mergeState } : listed;
|
|
6459
|
+
const rec = find();
|
|
6460
|
+
const patch = sendBackPatch(prNumber, pr, rec, repo.defaultBranch, note);
|
|
6461
|
+
this.setQa(rec, patch);
|
|
6462
|
+
setTimeout(() => this.schedule(), 200);
|
|
6463
|
+
this.toast("info", `PR #${prNumber} goes back to a developer${patch.fixReason === "conflict" ? " to resolve its conflicts" : ""}`);
|
|
6464
|
+
}
|
|
6465
|
+
buildQaSystemAppend(a, repo, cwd, branch, pr, testStep2, deps) {
|
|
6466
|
+
return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr, testStep: testStep2, depsLine: depsPromptLine(deps) });
|
|
5781
6467
|
}
|
|
5782
6468
|
async runQa(a, repo, rec) {
|
|
5783
6469
|
const branch = qaBranch(rec.prNumber, slugify(a.name));
|
|
@@ -5790,6 +6476,7 @@ Note from the manager: ${note}` : ""
|
|
|
5790
6476
|
);
|
|
5791
6477
|
let pr;
|
|
5792
6478
|
let issue2 = null;
|
|
6479
|
+
const lastTestedSha = rec.testedSha;
|
|
5793
6480
|
try {
|
|
5794
6481
|
pr = await this.backend.prDetails(repo.fullName, rec.prNumber);
|
|
5795
6482
|
const issueNumber = rec.issueNumber ?? pr.closesIssues[0] ?? null;
|
|
@@ -5813,22 +6500,29 @@ Note from the manager: ${note}` : ""
|
|
|
5813
6500
|
this.clearTask(a);
|
|
5814
6501
|
return;
|
|
5815
6502
|
}
|
|
5816
|
-
const
|
|
5817
|
-
if (!
|
|
6503
|
+
const desk = await this.prepare(a, repo, { pr: rec.prNumber }, branch);
|
|
6504
|
+
if (!desk) {
|
|
5818
6505
|
if (this.state.qa.includes(rec) && rec.status === "testing") this.setQa(rec, { status: "queued", qaAgentId: null });
|
|
5819
6506
|
return;
|
|
5820
6507
|
}
|
|
6508
|
+
const { cwd } = desk;
|
|
5821
6509
|
const dev = rec.devAgentId ? this.state.agents.find((x) => x.id === rec.devAgentId) : null;
|
|
6510
|
+
const failedConflict = rec.fixReason === "conflict" && !rec.passedSha;
|
|
6511
|
+
const qa = qaInstructions({
|
|
6512
|
+
...pr,
|
|
6513
|
+
round: rec.round,
|
|
6514
|
+
fixReason: failedConflict ? "qa" : rec.fixReason,
|
|
6515
|
+
lastTestedSha: failedConflict ? null : lastTestedSha,
|
|
6516
|
+
summary: rec.summary,
|
|
6517
|
+
fixInstructions: rec.fixInstructions,
|
|
6518
|
+
defaultBranch: repo.defaultBranch
|
|
6519
|
+
});
|
|
5822
6520
|
const prompt = [
|
|
5823
6521
|
`Please QA pull request #${pr.number}: ${pr.title}`,
|
|
5824
6522
|
`URL: ${pr.url}`,
|
|
5825
6523
|
`Author: ${dev ? `${dev.name} (developer agent)` : "a teammate"} \xB7 QA round ${rec.round}`,
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
This is a re-test after fixes. Last round's findings:
|
|
5829
|
-
${rec.summary}
|
|
5830
|
-
${rec.fixInstructions ?? ""}
|
|
5831
|
-
Check those first, then re-check everything else.` : "",
|
|
6524
|
+
qa.checks,
|
|
6525
|
+
qa.retest,
|
|
5832
6526
|
"",
|
|
5833
6527
|
"PR description:",
|
|
5834
6528
|
pr.body.trim() || "(empty)",
|
|
@@ -5836,7 +6530,7 @@ Check those first, then re-check everything else.` : "",
|
|
|
5836
6530
|
Linked issue: ${issue2.title}
|
|
5837
6531
|
${issue2.body.trim() || "(no description)"}` : ""
|
|
5838
6532
|
].filter((l) => l !== "").join("\n");
|
|
5839
|
-
this.startAgentSession(a, repo, cwd, prompt, this.buildQaSystemAppend(a, repo, cwd, branch, pr), void 0, QA_SCHEMA);
|
|
6533
|
+
this.startAgentSession(a, repo, cwd, prompt, this.buildQaSystemAppend(a, repo, cwd, branch, pr, qa.testStep, desk.deps), void 0, QA_SCHEMA);
|
|
5840
6534
|
}
|
|
5841
6535
|
async onQaFinished(a, repo, result) {
|
|
5842
6536
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
@@ -5860,22 +6554,25 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5860
6554
|
this.appendLog(a, [{ kind: pass ? "done" : "error", text: `${pass ? "\u2705 QA passed" : "\u274C QA failed"} PR #${a.prNumber} \xB7 ${report.checks.length} checks \xB7 ${rt.shots.length} screenshots` }]);
|
|
5861
6555
|
let commentUrl = null;
|
|
5862
6556
|
if (rec) {
|
|
6557
|
+
const pull = !pass && lastQaRound(rec) ? await this.backend.prDetails(repo.fullName, rec.prNumber).catch(() => null) : null;
|
|
6558
|
+
const next = qaOutcome(pass, rec, pull);
|
|
5863
6559
|
try {
|
|
5864
6560
|
this.appendLog(a, [{ kind: "system", text: "\u{1F4CE} Uploading evidence and posting the QA report on the PR\u2026" }]);
|
|
5865
|
-
const
|
|
5866
|
-
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber,
|
|
6561
|
+
const body2 = await this.renderQaComment(a, repo, rec, report, rt.shots, next);
|
|
6562
|
+
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber, body2) || null;
|
|
5867
6563
|
this.appendLog(a, [{ kind: "system", text: ` \u23BF ${commentUrl ?? "comment posted"}` }]);
|
|
5868
6564
|
} catch (err) {
|
|
5869
6565
|
this.appendLog(a, [{ kind: "error", text: ` \u23BF Could not post the QA report: ${err.message}` }]);
|
|
5870
6566
|
}
|
|
5871
6567
|
const qaRounds = rec.round - rec.retests;
|
|
5872
|
-
const nextStatus =
|
|
6568
|
+
const nextStatus = next === "conflict" ? "failed" : next;
|
|
6569
|
+
const fixInstructions = report.fixInstructions ?? report.checks.filter((c) => c.result === "fail").map((c) => `${c.name}: ${c.details}`).join("\n");
|
|
5873
6570
|
this.setQa(rec, {
|
|
5874
6571
|
status: nextStatus,
|
|
5875
6572
|
summary: report.summary,
|
|
5876
6573
|
checks: report.checks,
|
|
5877
6574
|
commentUrl: commentUrl ?? rec.commentUrl,
|
|
5878
|
-
fixInstructions
|
|
6575
|
+
fixInstructions,
|
|
5879
6576
|
sessionFailures: 0,
|
|
5880
6577
|
fixReason: pass ? null : "qa",
|
|
5881
6578
|
passedSha: pass ? rec.testedSha : null,
|
|
@@ -5884,13 +6581,14 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5884
6581
|
mergeRetryAt: null,
|
|
5885
6582
|
alerted: false
|
|
5886
6583
|
});
|
|
6584
|
+
if (next === "conflict") this.sendBack(repo, rec, "conflict", conflictFixInstructions(fixInstructions, repo.defaultBranch), false);
|
|
5887
6585
|
this.toast(
|
|
5888
6586
|
pass ? "success" : "error",
|
|
5889
|
-
pass ? `\u2705 ${a.name} passed PR #${rec.prNumber}${repo.autoMerge ? "; it merges once GitHub's checks are green" : ": ready to merge"}` : nextStatus === "needs-human" ? `\u274C PR #${rec.prNumber} failed QA ${qaRounds} times and needs a human` : `\u274C ${a.name} failed PR #${rec.prNumber}; sending it back to the developer`
|
|
6587
|
+
pass ? `\u2705 ${a.name} passed PR #${rec.prNumber}${repo.autoMerge ? "; it merges once GitHub's checks are green" : ": ready to merge"}` : nextStatus === "needs-human" ? `\u274C PR #${rec.prNumber} failed QA ${qaRounds} times and needs a human` : next === "conflict" ? `\u274C ${a.name} failed PR #${rec.prNumber}, and it conflicts with ${repo.defaultBranch}; sending it back to merge and fix` : `\u274C ${a.name} failed PR #${rec.prNumber}; sending it back to the developer`
|
|
5890
6588
|
);
|
|
5891
6589
|
}
|
|
5892
6590
|
}
|
|
5893
|
-
async renderQaComment(a, repo, rec, report, shots) {
|
|
6591
|
+
async renderQaComment(a, repo, rec, report, shots, next) {
|
|
5894
6592
|
const pass = report.verdict === "pass";
|
|
5895
6593
|
const images = [];
|
|
5896
6594
|
const evidence = shots.slice(-8);
|
|
@@ -5928,7 +6626,7 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5928
6626
|
if (images.length) lines.push("", "### \u{1F4F8} Evidence", "", ...images.flatMap((img) => [img, ""]));
|
|
5929
6627
|
else lines.push("", "_No browser screenshots were taken in this round._");
|
|
5930
6628
|
const merge = repo.autoMerge ? "merges automatically once GitHub's checks pass" : "ready for the manager to merge";
|
|
5931
|
-
lines.push("", `<sub>Posted by cubefarm \xB7 ${pass ? merge :
|
|
6629
|
+
lines.push("", `<sub>Posted by cubefarm \xB7 ${pass ? merge : next === "needs-human" ? "needs a human decision" : next === "conflict" ? `sent back to the developer to merge ${repo.defaultBranch} and fix` : "sent back to the developer for fixes"}</sub>`);
|
|
5932
6630
|
return lines.join("\n");
|
|
5933
6631
|
}
|
|
5934
6632
|
/** Send a failed PR back to the developer who wrote it (or any free developer on the floor). */
|
|
@@ -5945,16 +6643,20 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5945
6643
|
rec.fixReason === "conflict" ? `Resolving conflicts on PR #${rec.prNumber}` : rec.fixReason === "checks" ? `Fixing checks on PR #${rec.prNumber}` : `Fixing PR #${rec.prNumber} after QA round ${rec.round}`,
|
|
5946
6644
|
`Checking out PR #${rec.prNumber}\u2026`
|
|
5947
6645
|
);
|
|
5948
|
-
const
|
|
5949
|
-
if (!
|
|
6646
|
+
const desk = await this.prepare(dev, repo, { pr: rec.prNumber }, headRef);
|
|
6647
|
+
if (!desk) {
|
|
5950
6648
|
if (rec.status === "fixing") this.setQa(rec, { status: "failed" });
|
|
5951
6649
|
return;
|
|
5952
6650
|
}
|
|
6651
|
+
const { cwd } = desk;
|
|
5953
6652
|
const failed2 = rec.checks.filter((c) => c.result === "fail");
|
|
5954
6653
|
const takeover = original ? "" : " A teammate wrote it, so read the PR and the linked issue first.";
|
|
5955
6654
|
const push = `push to the same branch: git push origin HEAD:${headRef}`;
|
|
5956
|
-
const mergeFix = rec.fixReason === "conflict" ? [
|
|
6655
|
+
const mergeFix = rec.fixReason === "conflict" && rec.passedSha ? [
|
|
5957
6656
|
`QA passed pull request #${rec.prNumber} (${pull?.url ?? ""}), but it now conflicts with ${repo.defaultBranch} because other work was merged first.${takeover}`,
|
|
6657
|
+
rec.fixInstructions ? `
|
|
6658
|
+
What needs fixing:
|
|
6659
|
+
${rec.fixInstructions}` : "",
|
|
5958
6660
|
"",
|
|
5959
6661
|
`Bring it up to date: git fetch origin && git merge origin/${repo.defaultBranch}. Resolve the conflicts so both this change and the newly merged work keep working, run the project's checks, and ${push}`
|
|
5960
6662
|
] : rec.fixReason === "checks" ? [
|
|
@@ -5984,7 +6686,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5984
6686
|
const mergeEnd = "Then reply with a short summary of what you did. Do not open a new pull request; the office merges it once the checks pass, after another QA round if the code changed.";
|
|
5985
6687
|
const prompt = (mergeFix ? [...mergeFix, "", mergeEnd] : qaFix).filter((l) => l !== "").join("\n");
|
|
5986
6688
|
const resume = original && rec.devSessionId ? rec.devSessionId : void 0;
|
|
5987
|
-
this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }), resume);
|
|
6689
|
+
this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }, desk.deps), resume);
|
|
5988
6690
|
}
|
|
5989
6691
|
async onFixFinished(a, repo, result) {
|
|
5990
6692
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
@@ -6060,30 +6762,31 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6060
6762
|
this.startAgentSession(a, repo, cwd, text, this.buildSystemAppend(a, repo, cwd, a.branch, fixing), a.sessionId, void 0, typed ? "typed" : null);
|
|
6061
6763
|
}
|
|
6062
6764
|
updateSettings(patch) {
|
|
6063
|
-
const
|
|
6064
|
-
if (patch.sessionLimit !== void 0)
|
|
6065
|
-
if (isCli(patch.defaultCli) && patch.defaultCli !==
|
|
6066
|
-
|
|
6067
|
-
if (patch.defaultModel === void 0)
|
|
6068
|
-
}
|
|
6069
|
-
if (patch.defaultModel !== void 0)
|
|
6070
|
-
if (patch.defaultEffort !== void 0 && EFFORTS.includes(patch.defaultEffort))
|
|
6071
|
-
if (patch.runtime === "terminal" || patch.runtime === "sdk")
|
|
6072
|
-
if (patch.hiring === "approve" || patch.hiring === "auto")
|
|
6073
|
-
if (patch.teamCap !== void 0)
|
|
6074
|
-
if (patch.ceoHeartbeatMin !== void 0)
|
|
6075
|
-
if (typeof patch.managerName === "string")
|
|
6076
|
-
if (typeof patch.companyName === "string")
|
|
6077
|
-
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim())
|
|
6078
|
-
if (typeof patch.setupDone === "boolean")
|
|
6079
|
-
if (patch.tutorialStep !== void 0)
|
|
6080
|
-
if (typeof patch.autoUpdate === "boolean")
|
|
6081
|
-
if (patch.pacingSessions !== void 0)
|
|
6765
|
+
const s2 = this.state.settings;
|
|
6766
|
+
if (patch.sessionLimit !== void 0) s2.sessionLimit = Math.max(0, Math.round(Number(patch.sessionLimit)) || 0);
|
|
6767
|
+
if (isCli(patch.defaultCli) && patch.defaultCli !== s2.defaultCli) {
|
|
6768
|
+
s2.defaultCli = patch.defaultCli;
|
|
6769
|
+
if (patch.defaultModel === void 0) s2.defaultModel = s2.defaultCli === "claude" ? DEFAULT_MODEL : "";
|
|
6770
|
+
}
|
|
6771
|
+
if (patch.defaultModel !== void 0) s2.defaultModel = String(patch.defaultModel).trim() || (s2.defaultCli === "claude" ? DEFAULT_MODEL : "");
|
|
6772
|
+
if (patch.defaultEffort !== void 0 && EFFORTS.includes(patch.defaultEffort)) s2.defaultEffort = patch.defaultEffort;
|
|
6773
|
+
if (patch.runtime === "terminal" || patch.runtime === "sdk") s2.runtime = patch.runtime;
|
|
6774
|
+
if (patch.hiring === "approve" || patch.hiring === "auto") s2.hiring = patch.hiring;
|
|
6775
|
+
if (patch.teamCap !== void 0) s2.teamCap = Math.max(1, Math.min(15, Math.round(Number(patch.teamCap)) || 1));
|
|
6776
|
+
if (patch.ceoHeartbeatMin !== void 0) s2.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
6777
|
+
if (typeof patch.managerName === "string") s2.managerName = patch.managerName.trim().slice(0, 40);
|
|
6778
|
+
if (typeof patch.companyName === "string") s2.companyName = patch.companyName.trim().slice(0, 60);
|
|
6779
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s2.projectsDir = path15.resolve(patch.projectsDir.trim());
|
|
6780
|
+
if (typeof patch.setupDone === "boolean") s2.setupDone = patch.setupDone;
|
|
6781
|
+
if (patch.tutorialStep !== void 0) s2.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
6782
|
+
if (typeof patch.autoUpdate === "boolean") s2.autoUpdate = patch.autoUpdate;
|
|
6783
|
+
if (patch.pacingSessions !== void 0) s2.pacingSessions = clampPacingSessions(patch.pacingSessions);
|
|
6784
|
+
if (patch.voice !== void 0) s2.voice = voiceSettings(s2.voice, patch.voice);
|
|
6082
6785
|
this.save();
|
|
6083
|
-
this.broadcast({ type: "settings", settings:
|
|
6786
|
+
this.broadcast({ type: "settings", settings: s2 });
|
|
6084
6787
|
this.emitCeo();
|
|
6085
6788
|
setTimeout(() => this.schedule(), 200);
|
|
6086
|
-
return
|
|
6789
|
+
return s2;
|
|
6087
6790
|
}
|
|
6088
6791
|
/** The setup wizard: who you are, the company, and your CEO. */
|
|
6089
6792
|
setup(x) {
|
|
@@ -6134,8 +6837,8 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6134
6837
|
const ready = this.readyIssues(repo).map((r) => r.want);
|
|
6135
6838
|
const open = this.repoRt.get(repo.id).issues.map((i) => issueSpecialty(i.labels));
|
|
6136
6839
|
const rank = (a) => {
|
|
6137
|
-
const
|
|
6138
|
-
return [suits(a) ? 0 : 1, ready.filter((w) => w ===
|
|
6840
|
+
const s2 = a.specialty.toLowerCase();
|
|
6841
|
+
return [suits(a) ? 0 : 1, ready.filter((w) => w === s2).length, open.filter((w) => w === s2).length, a.desk];
|
|
6139
6842
|
};
|
|
6140
6843
|
return devs.map((a) => ({ a, r: rank(a) })).sort((x, y) => x.r[0] - y.r[0] || x.r[1] - y.r[1] || x.r[2] - y.r[2] || x.r[3] - y.r[3])[0].a;
|
|
6141
6844
|
}
|
|
@@ -6503,8 +7206,8 @@ ${job.text}` } : job;
|
|
|
6503
7206
|
void this.runCeoJob(a, job);
|
|
6504
7207
|
}
|
|
6505
7208
|
ceoPromptInput(a) {
|
|
6506
|
-
const
|
|
6507
|
-
return { name: a.name, company:
|
|
7209
|
+
const s2 = this.state.settings;
|
|
7210
|
+
return { name: a.name, company: s2.companyName, manager: s2.managerName, notesFile: path15.join(CEO_DIR, "NOTES.md"), sessionLimit: s2.sessionLimit, teamCap: s2.teamCap, hiring: s2.hiring };
|
|
6508
7211
|
}
|
|
6509
7212
|
async runCeoJob(a, job) {
|
|
6510
7213
|
const rt = this.agentRt.get(a.id);
|
|
@@ -6525,7 +7228,7 @@ ${job.text}` } : job;
|
|
|
6525
7228
|
this.emitAgent(a);
|
|
6526
7229
|
this.emitCeo();
|
|
6527
7230
|
this.save();
|
|
6528
|
-
await
|
|
7231
|
+
await fs13.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
6529
7232
|
if (a.status !== "working") {
|
|
6530
7233
|
this.state.ceo.job = null;
|
|
6531
7234
|
this.emitCeo();
|
|
@@ -6639,7 +7342,7 @@ ${t}`);
|
|
|
6639
7342
|
qa: this.state.qa.map((q) => [q.repoId, q.prNumber, q.status]),
|
|
6640
7343
|
requests: this.state.requests.filter((r) => r.status === "pending").map((r) => r.id)
|
|
6641
7344
|
};
|
|
6642
|
-
return
|
|
7345
|
+
return crypto7.createHash("sha1").update(JSON.stringify(data)).digest("hex");
|
|
6643
7346
|
}
|
|
6644
7347
|
maybeHeartbeat() {
|
|
6645
7348
|
const min = this.state.settings.ceoHeartbeatMin;
|
|
@@ -6752,7 +7455,7 @@ ${t}`);
|
|
|
6752
7455
|
return !BUSY.includes(a.status) ? null : a.task === "qa" ? `testing PR #${a.prNumber}` : a.task === "fix" ? `fixing PR #${a.prNumber}` : `issue #${a.issueNumber}`;
|
|
6753
7456
|
}
|
|
6754
7457
|
companyStatus() {
|
|
6755
|
-
const
|
|
7458
|
+
const s2 = this.state.settings;
|
|
6756
7459
|
const doing = (a) => this.agentDoing(a);
|
|
6757
7460
|
const jobDescription = (brief) => {
|
|
6758
7461
|
if (brief.length <= 400) return brief;
|
|
@@ -6829,9 +7532,9 @@ ${t}`);
|
|
|
6829
7532
|
{
|
|
6830
7533
|
company: {
|
|
6831
7534
|
ceo: this.ceo().name,
|
|
6832
|
-
hiring:
|
|
6833
|
-
teamCap:
|
|
6834
|
-
sessionLimit:
|
|
7535
|
+
hiring: s2.hiring === "auto" ? `auto-approved while a floor has fewer than ${s2.teamCap} people` : "the manager approves every proposal",
|
|
7536
|
+
teamCap: s2.teamCap,
|
|
7537
|
+
sessionLimit: s2.sessionLimit || "none",
|
|
6835
7538
|
sessionsRunning: this.running(),
|
|
6836
7539
|
usage: usageLabel(this.usageNow(), Date.now()),
|
|
6837
7540
|
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa },
|
|
@@ -6905,7 +7608,7 @@ ${t}`);
|
|
|
6905
7608
|
if (seated + waiting >= MAX_DESKS[role]) throw new Error(role === "qa" ? `The QA lab on floor ${repo.floor} is full.` : `Floor ${repo.floor} has no free desks.`);
|
|
6906
7609
|
const name = this.freeName(role);
|
|
6907
7610
|
const req = {
|
|
6908
|
-
id:
|
|
7611
|
+
id: crypto7.randomUUID(),
|
|
6909
7612
|
kind: "hire",
|
|
6910
7613
|
repoId: repo.id,
|
|
6911
7614
|
role,
|
|
@@ -6928,8 +7631,8 @@ ${t}`);
|
|
|
6928
7631
|
decidedBy: null
|
|
6929
7632
|
};
|
|
6930
7633
|
this.addRequest(req);
|
|
6931
|
-
const
|
|
6932
|
-
if (
|
|
7634
|
+
const s2 = this.state.settings;
|
|
7635
|
+
if (s2.hiring === "auto" && this.state.agents.filter((a) => a.repoId === repo.id).length < s2.teamCap) {
|
|
6933
7636
|
this.approveRequest(req.id, {}, "auto");
|
|
6934
7637
|
return `Hired ${req.name} as ${title} on floor ${repo.floor} (auto-approved; agent id ${req.agentId}).`;
|
|
6935
7638
|
}
|
|
@@ -6947,7 +7650,7 @@ ${t}`);
|
|
|
6947
7650
|
if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
|
|
6948
7651
|
checkPendingLimit(pending2.length);
|
|
6949
7652
|
const req = {
|
|
6950
|
-
id:
|
|
7653
|
+
id: crypto7.randomUUID(),
|
|
6951
7654
|
kind: "let-go",
|
|
6952
7655
|
repoId: repo.id,
|
|
6953
7656
|
role: a.role,
|
|
@@ -6983,11 +7686,11 @@ ${t}`);
|
|
|
6983
7686
|
const title = String(x.title ?? "").trim().slice(0, 120);
|
|
6984
7687
|
if (!title) throw new Error("An issue needs a title.");
|
|
6985
7688
|
const slug = specialtySlug(x.specialty);
|
|
6986
|
-
const
|
|
7689
|
+
const body2 = `${String(x.body ?? "").trim()}
|
|
6987
7690
|
|
|
6988
7691
|
---
|
|
6989
7692
|
_Filed by ${this.ceo().name}, the cubefarm CEO._`;
|
|
6990
|
-
const n = await this.backend.createIssue(repo.fullName, title,
|
|
7693
|
+
const n = await this.backend.createIssue(repo.fullName, title, body2, slug ? [specialtyLabel(slug)] : []);
|
|
6991
7694
|
this.ceoIssues.record(repo.id);
|
|
6992
7695
|
return `Filed #${n} on floor ${repo.floor}: ${title}${slug ? ` (routed to ${slug})` : ""}.`;
|
|
6993
7696
|
}
|
|
@@ -7108,6 +7811,7 @@ app.delete("/api/repos/:repo/preview", route((req) => swarm.stopPreview(repoId(r
|
|
|
7108
7811
|
app.post("/api/repos/:repo/pulls/:n/merge", route((req) => swarm.mergePull(repoId(req), num(req.params.n), req.body?.method ?? "squash")));
|
|
7109
7812
|
app.post("/api/repos/:repo/pulls/:n/close", route((req) => swarm.closePull(repoId(req), num(req.params.n))));
|
|
7110
7813
|
app.post("/api/repos/:repo/pulls/:n/qa", route((req) => swarm.sendToQa(repoId(req), num(req.params.n))));
|
|
7814
|
+
app.post("/api/repos/:repo/pulls/:n/fix", route((req) => swarm.sendBackToDev(repoId(req), num(req.params.n), parseSendBackNote(req.body))));
|
|
7111
7815
|
app.post(
|
|
7112
7816
|
"/api/repos/:repo/agents",
|
|
7113
7817
|
route(
|
|
@@ -7132,12 +7836,32 @@ app.post("/api/agents/:id/message", route((req) => swarm.message(String(req.para
|
|
|
7132
7836
|
app.get("/api/agents/:id/prompt", route((req) => swarm.agentPrompt(String(req.params.id))));
|
|
7133
7837
|
app.get("/api/agents/:id/screen", (req, res) => {
|
|
7134
7838
|
const shot = swarm.screenshot(String(req.params.id));
|
|
7135
|
-
if (!shot) return void res.status(404).end();
|
|
7136
|
-
res.setHeader("Content-Type", shot.mime);
|
|
7137
7839
|
res.setHeader("Cache-Control", "no-store");
|
|
7840
|
+
if (!shot) return void res.status(screenStatus(shot)).end();
|
|
7841
|
+
res.setHeader("Content-Type", shot.mime);
|
|
7138
7842
|
res.end(shot.data);
|
|
7139
7843
|
});
|
|
7140
7844
|
app.patch("/api/settings", route((req) => swarm.updateSettings(req.body ?? {})));
|
|
7845
|
+
var parse = (schema, value) => {
|
|
7846
|
+
const r = schema.safeParse(value);
|
|
7847
|
+
if (!r.success) throw new HttpError(400, r.error.issues.map((i) => `${i.path.join(".") || "request"}: ${i.message}`).join("; "));
|
|
7848
|
+
return r.data;
|
|
7849
|
+
};
|
|
7850
|
+
var sendAudio = (res, audio) => {
|
|
7851
|
+
res.setHeader("Content-Type", audio.subarray(0, 4).toString("latin1") === "RIFF" ? "audio/wav" : "audio/mpeg");
|
|
7852
|
+
res.setHeader("Cache-Control", "private, max-age=3600");
|
|
7853
|
+
res.end(audio);
|
|
7854
|
+
};
|
|
7855
|
+
app.put("/api/voice/key", route((req) => swarm.voice.setKey(parse(z3.object({ key: z3.string().max(400) }), req.body).key)));
|
|
7856
|
+
app.get("/api/voice/voices", route(() => swarm.voice.voices()));
|
|
7857
|
+
app.get(
|
|
7858
|
+
"/api/voice/messages/:id",
|
|
7859
|
+
route(async (req, res) => sendAudio(res, await swarm.voice.messageAudio(parse(z3.object({ id: z3.coerce.number().int().positive() }), req.params).id)))
|
|
7860
|
+
);
|
|
7861
|
+
app.get(
|
|
7862
|
+
"/api/voice/sample",
|
|
7863
|
+
route(async (req, res) => sendAudio(res, await swarm.voice.sampleAudio(parse(z3.object({ voiceId: z3.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "not a voice id").optional() }), req.query).voiceId)))
|
|
7864
|
+
);
|
|
7141
7865
|
app.post("/api/office/update", route((req) => swarm.updateOffice(req.body?.action)));
|
|
7142
7866
|
app.post("/api/ceo/message", route((req) => swarm.messageCeo(str(req.body.text))));
|
|
7143
7867
|
app.post("/api/ceo/review", route(() => swarm.requestReview()));
|
|
@@ -7147,10 +7871,10 @@ app.post(
|
|
|
7147
7871
|
route((req) => swarm.approveRequest(String(req.params.id), { name: str(req.body?.name) || void 0, model: typeof req.body?.model === "string" ? req.body.model : void 0, effort: typeof req.body?.effort === "string" ? req.body.effort : void 0 }))
|
|
7148
7872
|
);
|
|
7149
7873
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
7150
|
-
var dist =
|
|
7151
|
-
if (
|
|
7874
|
+
var dist = path16.resolve(import.meta.dirname, "../dist");
|
|
7875
|
+
if (fs14.existsSync(dist)) {
|
|
7152
7876
|
app.use(express.static(dist));
|
|
7153
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
7877
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path16.join(dist, "index.html")));
|
|
7154
7878
|
}
|
|
7155
7879
|
app.use((err, _req, res, _next) => {
|
|
7156
7880
|
const parserStatus = err?.status;
|