cubefarm 0.3.9 → 0.3.11
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-Db6Gj9A-.js +2 -0
- package/dist/assets/index-DZng_ER1.js +4710 -0
- package/dist/assets/index-qBB5_EtY.css +1 -0
- package/dist/assets/{rapier--PAOxOZs.js → rapier-aQSokaY9.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +1035 -340
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/assets/ToyWorld-CCa77hRS.js +0 -2
- package/dist/assets/index-CIr98Zaa.css +0 -1
- package/dist/assets/index-g6QWWXij.js +0 -4331
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]);
|
|
@@ -163,8 +164,8 @@ async function issueState(fullName, number) {
|
|
|
163
164
|
return null;
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
|
-
async function closeIssue(fullName, number, comment) {
|
|
167
|
-
await gh(["issue", "close", String(number), "-R", fullName, "--reason",
|
|
167
|
+
async function closeIssue(fullName, number, comment, reason2 = "completed") {
|
|
168
|
+
await gh(["issue", "close", String(number), "-R", fullName, "--reason", reason2, "--comment", comment]);
|
|
168
169
|
}
|
|
169
170
|
async function editIssue(fullName, number, edit) {
|
|
170
171
|
const args = ["issue", "edit", String(number), "-R", fullName];
|
|
@@ -187,6 +188,9 @@ async function mergePull(fullName, number, method, headSha) {
|
|
|
187
188
|
async function updateBranch(fullName, number) {
|
|
188
189
|
await gh(["pr", "update-branch", String(number), "-R", fullName], { timeoutMs: 6e4 });
|
|
189
190
|
}
|
|
191
|
+
async function rerunFailedJobs(fullName, runIds) {
|
|
192
|
+
for (const id of runIds) await gh(["run", "rerun", String(id), "--failed", "-R", fullName], { timeoutMs: 6e4 });
|
|
193
|
+
}
|
|
190
194
|
async function closePull(fullName, number) {
|
|
191
195
|
await gh(["pr", "close", String(number), "-R", fullName]);
|
|
192
196
|
}
|
|
@@ -220,13 +224,13 @@ async function issueDetails(fullName, number) {
|
|
|
220
224
|
const raw = await ghJson(["issue", "view", String(number), "-R", fullName, "--json", "title,body"]);
|
|
221
225
|
return { title: raw.title, body: raw.body ?? "" };
|
|
222
226
|
}
|
|
223
|
-
async function commentPull(fullName, number,
|
|
224
|
-
const out = await gh(["pr", "comment", String(number), "-R", fullName, "--body-file", "-"], { input:
|
|
227
|
+
async function commentPull(fullName, number, body2) {
|
|
228
|
+
const out = await gh(["pr", "comment", String(number), "-R", fullName, "--body-file", "-"], { input: body2 });
|
|
225
229
|
return out.match(/https:\/\/github\.com\/\S+/)?.[0] ?? "";
|
|
226
230
|
}
|
|
227
231
|
var EVIDENCE_BRANCH = "swarm-qa-evidence";
|
|
228
232
|
var evidenceReady = /* @__PURE__ */ new Set();
|
|
229
|
-
var ghApiJson = (method, endpoint,
|
|
233
|
+
var ghApiJson = (method, endpoint, body2) => gh(["api", "-X", method, endpoint, "-H", "Content-Type: application/json", "--input", "-"], { input: JSON.stringify(body2), timeoutMs: 12e4 }).then(
|
|
230
234
|
(out) => out ? JSON.parse(out) : null
|
|
231
235
|
);
|
|
232
236
|
async function ensureEvidenceBranch(fullName) {
|
|
@@ -819,14 +823,14 @@ function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
|
819
823
|
}
|
|
820
824
|
return found2;
|
|
821
825
|
}
|
|
822
|
-
var clip = (
|
|
826
|
+
var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
|
|
823
827
|
function rel(cwd, p) {
|
|
824
|
-
const
|
|
825
|
-
if (!
|
|
826
|
-
const r = path4.relative(cwd, path4.resolve(cwd,
|
|
827
|
-
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") :
|
|
828
|
+
const s2 = String(p ?? "");
|
|
829
|
+
if (!s2) return "";
|
|
830
|
+
const r = path4.relative(cwd, path4.resolve(cwd, s2));
|
|
831
|
+
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s2;
|
|
828
832
|
}
|
|
829
|
-
var escapeRe = (
|
|
833
|
+
var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
830
834
|
function tidyPaths(cwd, text) {
|
|
831
835
|
let out = text;
|
|
832
836
|
for (const v of /* @__PURE__ */ new Set([cwd, cwd.replaceAll("\\", "/")])) {
|
|
@@ -892,6 +896,8 @@ function describeOfficeTool(action, input) {
|
|
|
892
896
|
return `\u{1F44B} propose_let_go ${String(input.agent_id ?? "")}`;
|
|
893
897
|
case "file_issue":
|
|
894
898
|
return `\u{1F4DD} file_issue "${clip(String(input.title ?? ""), 70)}"${floor}${input.specialty ? ` \xB7 ${input.specialty}` : ""}`;
|
|
899
|
+
case "close_issue":
|
|
900
|
+
return `\u{1F5C2}\uFE0F close_issue #${String(input.number ?? "?")}${floor}`;
|
|
895
901
|
case "route_issue":
|
|
896
902
|
return `\u{1F500} route_issue #${String(input.number ?? "?")}${floor}${input.specialty !== void 0 ? ` \xB7 ${input.specialty || "no specialty"}` : ""}${Array.isArray(input.depends_on) ? ` \xB7 depends on ${input.depends_on.map((n) => `#${n}`).join(", ") || "nothing"}` : ""}`;
|
|
897
903
|
}
|
|
@@ -1006,7 +1012,7 @@ function startSession(opts, callbacks) {
|
|
|
1006
1012
|
case "system": {
|
|
1007
1013
|
if (msg.subtype === "init") {
|
|
1008
1014
|
cb.sessionId(msg.session_id);
|
|
1009
|
-
const mcp = msg.mcp_servers.map((
|
|
1015
|
+
const mcp = msg.mcp_servers.map((s2) => `${s2.name}:${s2.status}`).join(", ");
|
|
1010
1016
|
cb.log([
|
|
1011
1017
|
{ kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort` },
|
|
1012
1018
|
{ kind: "system", text: ` cwd ${msg.cwd}${mcp ? ` \xB7 mcp ${mcp}` : ""}` }
|
|
@@ -1223,7 +1229,7 @@ async function detectClis() {
|
|
|
1223
1229
|
var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
|
|
1224
1230
|
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
|
|
1225
1231
|
function codexHookCommand(node, script, win = WIN) {
|
|
1226
|
-
return win ? `node "${script}"` : [node, script].map((
|
|
1232
|
+
return win ? `node "${script}"` : [node, script].map((s2) => `'${s2.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
1227
1233
|
}
|
|
1228
1234
|
function launchArgs(id, ctx) {
|
|
1229
1235
|
switch (id) {
|
|
@@ -1253,7 +1259,7 @@ function launchArgs(id, ctx) {
|
|
|
1253
1259
|
return { args, env: { DISABLE_AUTOUPDATER: "1" } };
|
|
1254
1260
|
}
|
|
1255
1261
|
case "codex": {
|
|
1256
|
-
const toml = (
|
|
1262
|
+
const toml = (s2) => JSON.stringify(s2);
|
|
1257
1263
|
const args = [
|
|
1258
1264
|
"--no-alt-screen",
|
|
1259
1265
|
"-c",
|
|
@@ -1657,11 +1663,11 @@ function setOfficeUrl(url) {
|
|
|
1657
1663
|
officeUrl = url;
|
|
1658
1664
|
}
|
|
1659
1665
|
var routes = /* @__PURE__ */ new Map();
|
|
1660
|
-
function handleHook(token,
|
|
1666
|
+
function handleHook(token, body2) {
|
|
1661
1667
|
const route2 = routes.get(token);
|
|
1662
|
-
if (!route2 || !
|
|
1668
|
+
if (!route2 || !body2 || typeof body2 !== "object" || Array.isArray(body2)) return {};
|
|
1663
1669
|
try {
|
|
1664
|
-
return route2.hook(
|
|
1670
|
+
return route2.hook(body2);
|
|
1665
1671
|
} catch (err) {
|
|
1666
1672
|
console.warn("hook failed", err);
|
|
1667
1673
|
return {};
|
|
@@ -1708,7 +1714,7 @@ function toolOutput(response) {
|
|
|
1708
1714
|
if (typeof response === "string") return { text: response, images };
|
|
1709
1715
|
const r = response;
|
|
1710
1716
|
if (r && typeof r === "object") {
|
|
1711
|
-
if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((
|
|
1717
|
+
if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((s2) => typeof s2 === "string" && s2).join("\n"), images };
|
|
1712
1718
|
if (typeof r.file?.content === "string") return { text: r.file.content, images };
|
|
1713
1719
|
if (Array.isArray(r.filenames)) return { text: r.filenames.join("\n"), images };
|
|
1714
1720
|
}
|
|
@@ -2191,19 +2197,137 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
2191
2197
|
return handle;
|
|
2192
2198
|
}
|
|
2193
2199
|
|
|
2194
|
-
// server/
|
|
2200
|
+
// server/deps.ts
|
|
2195
2201
|
import { spawn as spawn2 } from "node:child_process";
|
|
2196
2202
|
import crypto4 from "node:crypto";
|
|
2197
2203
|
import fs8 from "node:fs/promises";
|
|
2198
|
-
import net2 from "node:net";
|
|
2199
2204
|
import path9 from "node:path";
|
|
2205
|
+
var INSTALL_MARKER = ".cubefarm-preview-install";
|
|
2206
|
+
var INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
2207
|
+
var MAX_DESK_INSTALLS = 2;
|
|
2208
|
+
var depsHash = (pkg, lock) => crypto4.createHash("sha256").update(pkg).update("\0").update(lock ?? "").digest("hex");
|
|
2209
|
+
async function readDeps(dir) {
|
|
2210
|
+
const read = (file) => fs8.readFile(path9.join(dir, file), "utf8").catch(() => null);
|
|
2211
|
+
const [pkg, lock, marker, nodeModules] = await Promise.all([
|
|
2212
|
+
read("package.json"),
|
|
2213
|
+
read("package-lock.json"),
|
|
2214
|
+
read(path9.join("node_modules", INSTALL_MARKER)),
|
|
2215
|
+
fs8.stat(path9.join(dir, "node_modules")).then((s2) => s2.isDirectory()).catch(() => false)
|
|
2216
|
+
]);
|
|
2217
|
+
return { pkg, lock, hash: depsHash(pkg ?? "", lock), marker: marker ?? "", nodeModules };
|
|
2218
|
+
}
|
|
2219
|
+
var depsCurrent = (s2) => s2.nodeModules && s2.marker === s2.hash;
|
|
2220
|
+
var writeMarker = (dir, hash) => fs8.writeFile(path9.join(dir, "node_modules", INSTALL_MARKER), hash).catch(() => void 0);
|
|
2221
|
+
function deskDepsPlan(s2) {
|
|
2222
|
+
if (s2.pkg === null) return "none";
|
|
2223
|
+
if (s2.lock === null) return "agent";
|
|
2224
|
+
return depsCurrent(s2) ? "skip" : "install";
|
|
2225
|
+
}
|
|
2226
|
+
function depsPromptLine(outcome) {
|
|
2227
|
+
if (outcome === "installed" || outcome === "skipped") return "Dependencies are already installed for this checkout; run npm install only if you change package.json.";
|
|
2228
|
+
if (outcome === "failed") return "The office couldn't install this checkout's dependencies: run npm install yourself first.";
|
|
2229
|
+
return "";
|
|
2230
|
+
}
|
|
2231
|
+
function createLimiter(max) {
|
|
2232
|
+
let active = 0;
|
|
2233
|
+
const waiting = [];
|
|
2234
|
+
return async function limit(fn) {
|
|
2235
|
+
if (active >= max) await new Promise((r) => waiting.push(r));
|
|
2236
|
+
else active++;
|
|
2237
|
+
try {
|
|
2238
|
+
return await fn();
|
|
2239
|
+
} finally {
|
|
2240
|
+
const next = waiting.shift();
|
|
2241
|
+
if (next) next();
|
|
2242
|
+
else active--;
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
var deskInstalls = createLimiter(MAX_DESK_INSTALLS);
|
|
2247
|
+
function killTree2(child) {
|
|
2248
|
+
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
2249
|
+
if (process.platform === "win32") {
|
|
2250
|
+
return new Promise((resolve) => {
|
|
2251
|
+
const tk = spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
2252
|
+
tk.once("error", () => resolve());
|
|
2253
|
+
tk.once("close", () => resolve());
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
try {
|
|
2257
|
+
process.kill(-child.pid, "SIGKILL");
|
|
2258
|
+
} catch {
|
|
2259
|
+
child.kill("SIGKILL");
|
|
2260
|
+
}
|
|
2261
|
+
return Promise.resolve();
|
|
2262
|
+
}
|
|
2263
|
+
function npmCi(dir, timeoutMs = INSTALL_TIMEOUT_MS) {
|
|
2264
|
+
const env = {};
|
|
2265
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
2266
|
+
if (v !== void 0 && !/^(ANTHROPIC_|CLAUDE|SWARM_)/i.test(k)) env[k] = v;
|
|
2267
|
+
}
|
|
2268
|
+
env.NO_COLOR = "1";
|
|
2269
|
+
const win = process.platform === "win32";
|
|
2270
|
+
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"] });
|
|
2271
|
+
let tail = "";
|
|
2272
|
+
const keep = (buf) => tail = (tail + buf.toString()).slice(-4e3);
|
|
2273
|
+
child.stdout?.on("data", keep);
|
|
2274
|
+
child.stderr?.on("data", keep);
|
|
2275
|
+
return new Promise((resolve, reject) => {
|
|
2276
|
+
const timer = setTimeout(() => {
|
|
2277
|
+
void killTree2(child);
|
|
2278
|
+
reject(new Error(`npm ci took longer than ${Math.round(timeoutMs / 6e4)} minutes`));
|
|
2279
|
+
}, timeoutMs);
|
|
2280
|
+
child.once("error", (err) => {
|
|
2281
|
+
clearTimeout(timer);
|
|
2282
|
+
reject(err);
|
|
2283
|
+
});
|
|
2284
|
+
child.once("close", (code) => {
|
|
2285
|
+
clearTimeout(timer);
|
|
2286
|
+
if (code === 0) return resolve();
|
|
2287
|
+
const last = tail.split(/\r?\n/).filter((l) => l.trim()).slice(-3).join(" \xB7 ");
|
|
2288
|
+
reject(new Error(`npm ci failed (code ${code})${last ? `: ${last.slice(0, 300)}` : ""}`));
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
async function installDesk(dir, cb, install = npmCi) {
|
|
2293
|
+
const { log } = cb;
|
|
2294
|
+
try {
|
|
2295
|
+
const state = await readDeps(dir);
|
|
2296
|
+
const plan = deskDepsPlan(state);
|
|
2297
|
+
if (plan === "none") return "none";
|
|
2298
|
+
if (plan === "agent") {
|
|
2299
|
+
log(["No package-lock.json; leaving the dependencies to the agent."]);
|
|
2300
|
+
return "agent";
|
|
2301
|
+
}
|
|
2302
|
+
if (plan === "skip") {
|
|
2303
|
+
log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2304
|
+
return "skipped";
|
|
2305
|
+
}
|
|
2306
|
+
log(["Installing dependencies\u2026"]);
|
|
2307
|
+
cb.installing();
|
|
2308
|
+
await deskInstalls(async () => {
|
|
2309
|
+
log(["$ npm ci"]);
|
|
2310
|
+
await install(dir);
|
|
2311
|
+
});
|
|
2312
|
+
await writeMarker(dir, state.hash);
|
|
2313
|
+
log(["Dependencies installed."]);
|
|
2314
|
+
return "installed";
|
|
2315
|
+
} catch (err) {
|
|
2316
|
+
log([`\u26A0 Could not install the dependencies (${err.message.split(/\r?\n/)[0].slice(0, 300)}); the agent will run npm install itself.`]);
|
|
2317
|
+
return "failed";
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// server/previewRunner.ts
|
|
2322
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2323
|
+
import fs9 from "node:fs/promises";
|
|
2324
|
+
import net2 from "node:net";
|
|
2325
|
+
import path10 from "node:path";
|
|
2200
2326
|
var PREVIEW_SLUG = "preview";
|
|
2201
2327
|
var PREVIEW_BRANCH = "swarm-preview";
|
|
2202
2328
|
var PREVIEW_TMP = ".preview-tmp";
|
|
2203
|
-
var INSTALL_MARKER = ".cubefarm-preview-install";
|
|
2204
2329
|
var START_TIMEOUT_MS = 3 * 6e4;
|
|
2205
|
-
var
|
|
2206
|
-
var fillPlaceholders = (s, port, tmp) => s.replaceAll("{port}", String(port)).replaceAll("{tmp}", tmp);
|
|
2330
|
+
var fillPlaceholders = (s2, port, tmp) => s2.replaceAll("{port}", String(port)).replaceAll("{tmp}", tmp);
|
|
2207
2331
|
function previewEnv(extra, port, tmp) {
|
|
2208
2332
|
const env = {};
|
|
2209
2333
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -2232,35 +2356,19 @@ async function portOpen(port) {
|
|
|
2232
2356
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2233
2357
|
async function readJson(file) {
|
|
2234
2358
|
try {
|
|
2235
|
-
return JSON.parse(await
|
|
2359
|
+
return JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2236
2360
|
} catch {
|
|
2237
2361
|
return null;
|
|
2238
2362
|
}
|
|
2239
2363
|
}
|
|
2240
2364
|
function defaultCommand(pkg) {
|
|
2241
2365
|
const scripts = pkg?.scripts ?? {};
|
|
2242
|
-
const name = ["dev", "start", "preview"].find((
|
|
2366
|
+
const name = ["dev", "start", "preview"].find((s2) => typeof scripts[s2] === "string" && scripts[s2].trim());
|
|
2243
2367
|
if (!name) return null;
|
|
2244
2368
|
const script = scripts[name];
|
|
2245
2369
|
const vite = /^\s*vite(\s|$)/.test(script) && !/--port\b/.test(script);
|
|
2246
2370
|
return `npm run ${name}${vite ? " -- --port {port} --strictPort" : ""}`;
|
|
2247
2371
|
}
|
|
2248
|
-
function killTree2(child) {
|
|
2249
|
-
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
2250
|
-
if (process.platform === "win32") {
|
|
2251
|
-
return new Promise((resolve) => {
|
|
2252
|
-
const tk = spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
2253
|
-
tk.once("error", () => resolve());
|
|
2254
|
-
tk.once("close", () => resolve());
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
|
-
try {
|
|
2258
|
-
process.kill(-child.pid, "SIGKILL");
|
|
2259
|
-
} catch {
|
|
2260
|
-
child.kill("SIGKILL");
|
|
2261
|
-
}
|
|
2262
|
-
return Promise.resolve();
|
|
2263
|
-
}
|
|
2264
2372
|
var Stopped = class extends Error {
|
|
2265
2373
|
};
|
|
2266
2374
|
var RealPreview = class {
|
|
@@ -2293,9 +2401,9 @@ var RealPreview = class {
|
|
|
2293
2401
|
this.check();
|
|
2294
2402
|
await ensureLocalExcludes(job.fullName).catch(() => void 0);
|
|
2295
2403
|
cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
|
|
2296
|
-
const tmp =
|
|
2297
|
-
await
|
|
2298
|
-
const pkg = await readJson(
|
|
2404
|
+
const tmp = path10.join(wt, PREVIEW_TMP);
|
|
2405
|
+
await fs9.mkdir(tmp, { recursive: true });
|
|
2406
|
+
const pkg = await readJson(path10.join(wt, "package.json"));
|
|
2299
2407
|
const command = job.command ?? defaultCommand(pkg);
|
|
2300
2408
|
if (!command) {
|
|
2301
2409
|
this.stopped = true;
|
|
@@ -2334,15 +2442,12 @@ var RealPreview = class {
|
|
|
2334
2442
|
}
|
|
2335
2443
|
/** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
|
|
2336
2444
|
async install(wt, env) {
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
const hash = crypto4.createHash("sha256").update(await fs8.readFile(path9.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
|
|
2340
|
-
const marker = path9.join(wt, "node_modules", INSTALL_MARKER);
|
|
2341
|
-
if (await fs8.readFile(marker, "utf8").catch(() => "") === hash) {
|
|
2445
|
+
const deps = await readDeps(wt);
|
|
2446
|
+
if (depsCurrent(deps)) {
|
|
2342
2447
|
this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2343
2448
|
return;
|
|
2344
2449
|
}
|
|
2345
|
-
const cmd = lock !== null ? "npm ci" : "npm install";
|
|
2450
|
+
const cmd = deps.lock !== null ? "npm ci" : "npm install";
|
|
2346
2451
|
this.cb.log([`$ ${cmd}`]);
|
|
2347
2452
|
const code = await Promise.race([this.spawn(cmd, wt, env), sleep2(INSTALL_TIMEOUT_MS).then(() => "timeout")]);
|
|
2348
2453
|
this.check();
|
|
@@ -2352,12 +2457,12 @@ var RealPreview = class {
|
|
|
2352
2457
|
}
|
|
2353
2458
|
if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
|
|
2354
2459
|
this.child = null;
|
|
2355
|
-
await
|
|
2460
|
+
await writeMarker(wt, deps.hash);
|
|
2356
2461
|
}
|
|
2357
2462
|
/** Run a command line through the platform shell; resolves with its exit code. */
|
|
2358
2463
|
spawn(cmd, cwd, env) {
|
|
2359
2464
|
this.check();
|
|
2360
|
-
const child =
|
|
2465
|
+
const child = spawn3(cmd, { cwd, env, shell: true, windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
2361
2466
|
this.child = child;
|
|
2362
2467
|
const onData = (buf) => {
|
|
2363
2468
|
const text = this.partial + buf.toString().replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
@@ -2383,29 +2488,29 @@ var RealPreview = class {
|
|
|
2383
2488
|
};
|
|
2384
2489
|
var realPreviews = {
|
|
2385
2490
|
start: (job, cb) => new RealPreview(job, cb),
|
|
2386
|
-
hasDefault: async (fullName) =>
|
|
2491
|
+
hasDefault: async (fullName) => fs9.access(path10.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
2387
2492
|
};
|
|
2388
2493
|
|
|
2389
2494
|
// server/officeUpdate.ts
|
|
2390
|
-
import
|
|
2391
|
-
import
|
|
2495
|
+
import fs10 from "node:fs/promises";
|
|
2496
|
+
import path11 from "node:path";
|
|
2392
2497
|
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
2393
2498
|
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
2394
2499
|
function underLauncher(proc = process) {
|
|
2395
2500
|
return typeof proc.send === "function" && proc.env.SWARM_LAUNCHER === "1";
|
|
2396
2501
|
}
|
|
2397
|
-
function drainDecision(
|
|
2502
|
+
function drainDecision(s2) {
|
|
2398
2503
|
const idle = (state) => ({ state, drainingSince: null, send: false, timedOut: false });
|
|
2399
|
-
if (
|
|
2400
|
-
if (
|
|
2401
|
-
if (!
|
|
2402
|
-
const postponed =
|
|
2403
|
-
const failedHold =
|
|
2404
|
-
const due =
|
|
2405
|
-
if (!due) return idle(
|
|
2406
|
-
const since =
|
|
2407
|
-
const timedOut =
|
|
2408
|
-
return { state: "draining", drainingSince: since, send:
|
|
2504
|
+
if (s2.sent) return { state: "updating", drainingSince: s2.drainingSince, send: false, timedOut: false };
|
|
2505
|
+
if (s2.behind <= 0) return idle("none");
|
|
2506
|
+
if (!s2.launcher) return idle(s2.failed ? "failed" : "available");
|
|
2507
|
+
const postponed = s2.postponedUntil !== null && s2.now < s2.postponedUntil && s2.behind <= s2.postponedBehind;
|
|
2508
|
+
const failedHold = s2.failed !== null && s2.behind <= (s2.failedBehind ?? Infinity);
|
|
2509
|
+
const due = s2.requested || s2.autoUpdate && !postponed && !failedHold;
|
|
2510
|
+
if (!due) return idle(s2.failed ? "failed" : s2.autoUpdate && postponed ? "waiting" : "available");
|
|
2511
|
+
const since = s2.drainingSince ?? s2.now;
|
|
2512
|
+
const timedOut = s2.running > 0 && s2.now - since >= DRAIN_TIMEOUT_MS;
|
|
2513
|
+
return { state: "draining", drainingSince: since, send: s2.running === 0 || timedOut, timedOut };
|
|
2409
2514
|
}
|
|
2410
2515
|
function parseLastUpdate(raw) {
|
|
2411
2516
|
try {
|
|
@@ -2425,10 +2530,10 @@ function parseLastUpdate(raw) {
|
|
|
2425
2530
|
}
|
|
2426
2531
|
}
|
|
2427
2532
|
async function takeLastUpdate(home) {
|
|
2428
|
-
const file =
|
|
2429
|
-
const raw = await
|
|
2533
|
+
const file = path11.join(home, "last-update.json");
|
|
2534
|
+
const raw = await fs10.readFile(file, "utf8").catch(() => null);
|
|
2430
2535
|
if (raw === null) return null;
|
|
2431
|
-
await
|
|
2536
|
+
await fs10.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
2432
2537
|
return parseLastUpdate(raw);
|
|
2433
2538
|
}
|
|
2434
2539
|
function lastUpdateMessage(u, commits) {
|
|
@@ -2436,8 +2541,8 @@ function lastUpdateMessage(u, commits) {
|
|
|
2436
2541
|
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
2437
2542
|
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
2438
2543
|
}
|
|
2439
|
-
var OFFICE_DIR =
|
|
2440
|
-
var isSamePath = (a, b) => process.platform === "win32" ?
|
|
2544
|
+
var OFFICE_DIR = path11.resolve(import.meta.dirname, "..");
|
|
2545
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path11.resolve(a).toLowerCase() === path11.resolve(b).toLowerCase() : path11.resolve(a) === path11.resolve(b);
|
|
2441
2546
|
var realOffice = {
|
|
2442
2547
|
launcher: underLauncher(),
|
|
2443
2548
|
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
@@ -2451,6 +2556,340 @@ var realOffice = {
|
|
|
2451
2556
|
}
|
|
2452
2557
|
};
|
|
2453
2558
|
|
|
2559
|
+
// server/voice.ts
|
|
2560
|
+
import crypto5 from "node:crypto";
|
|
2561
|
+
import fs11 from "node:fs/promises";
|
|
2562
|
+
import path12 from "node:path";
|
|
2563
|
+
|
|
2564
|
+
// shared/speech.ts
|
|
2565
|
+
var SPEECH_MAX_CHARS = 1500;
|
|
2566
|
+
var SAMPLE_LINE = "Hi! This is how I'll sound when I message you from the office.";
|
|
2567
|
+
var EMOJI = /[\p{Extended_Pictographic}\p{Regional_Indicator}\u{1F3FB}-\u{1F3FF}︎️⃣]/gu;
|
|
2568
|
+
var URL2 = /\b(?:https?:\/\/|www\.)[^\s<>()]+[^\s<>().,;:!?'"]/gi;
|
|
2569
|
+
function speechText(text, max = SPEECH_MAX_CHARS) {
|
|
2570
|
+
let s2 = text.replace(/\r\n?/g, "\n");
|
|
2571
|
+
s2 = s2.replace(/```[\s\S]*?(```|$)/g, "\n");
|
|
2572
|
+
s2 = s2.replace(/`([^`\n]*)`/g, "$1");
|
|
2573
|
+
s2 = s2.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
2574
|
+
s2 = s2.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
2575
|
+
s2 = s2.replace(/<(https?:[^>\s]+)>/g, "$1");
|
|
2576
|
+
s2 = s2.replace(/<\/?[a-z][^>]*>/gi, "");
|
|
2577
|
+
s2 = s2.replace(URL2, "a link");
|
|
2578
|
+
s2 = s2.replace(EMOJI, "");
|
|
2579
|
+
s2 = s2.replace(/(\*\*|__)(.+?)\1/g, "$2");
|
|
2580
|
+
s2 = s2.replace(/(^|[^\w*])\*(?!\s)([^*\n]+?)\*(?!\w)/g, "$1$2");
|
|
2581
|
+
s2 = s2.replace(/(^|[^\w])_(?!\s)([^_\n]+?)_(?!\w)/g, "$1$2");
|
|
2582
|
+
s2 = s2.replace(/~~(.+?)~~/g, "$1");
|
|
2583
|
+
s2 = s2.replace(/#(\d+)\b/g, "number $1");
|
|
2584
|
+
const lines = [];
|
|
2585
|
+
for (let line of s2.split("\n")) {
|
|
2586
|
+
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();
|
|
2587
|
+
if (!line) continue;
|
|
2588
|
+
lines.push(/[.!?:;,…]$/.test(line) ? line : `${line}.`);
|
|
2589
|
+
}
|
|
2590
|
+
return cut(lines.join(" ").replace(/\s+([.,!?;:])/g, "$1"), max);
|
|
2591
|
+
}
|
|
2592
|
+
function cut(s2, max) {
|
|
2593
|
+
if (s2.length <= max) return s2;
|
|
2594
|
+
const head = s2.slice(0, max + 1);
|
|
2595
|
+
const ends = [...head.matchAll(/[.!?…](?=\s|$)/g)];
|
|
2596
|
+
const end = ends.length ? ends[ends.length - 1].index + 1 : -1;
|
|
2597
|
+
if (end >= max / 2) return s2.slice(0, end);
|
|
2598
|
+
const space = s2.lastIndexOf(" ", max);
|
|
2599
|
+
return `${s2.slice(0, space > 0 ? space : max).replace(/[,;:\s]+$/, "")}\u2026`;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// server/httpError.ts
|
|
2603
|
+
var HttpError = class extends Error {
|
|
2604
|
+
constructor(status, message) {
|
|
2605
|
+
super(message);
|
|
2606
|
+
this.status = status;
|
|
2607
|
+
}
|
|
2608
|
+
status;
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
// server/voice.ts
|
|
2612
|
+
var DEFAULT_VOICE_MODEL = "eleven_flash_v2_5";
|
|
2613
|
+
var VOICES_TTL_MS = 10 * 6e4;
|
|
2614
|
+
var SYNTH_TIMEOUT_MS = 2e4;
|
|
2615
|
+
var CACHE_MAX_FILES = 200;
|
|
2616
|
+
var CACHE_MAX_AGE_MS = 7 * 24 * 60 * 6e4;
|
|
2617
|
+
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.";
|
|
2618
|
+
var voice = (id, name, description, accent = "") => ({
|
|
2619
|
+
id,
|
|
2620
|
+
name,
|
|
2621
|
+
category: "library",
|
|
2622
|
+
labels: { accent, gender: "", age: "", description, use_case: "conversational" },
|
|
2623
|
+
previewUrl: null,
|
|
2624
|
+
recommended: true
|
|
2625
|
+
});
|
|
2626
|
+
var RECOMMENDED_VOICES = [
|
|
2627
|
+
voice("OZ0L6eISlOejga3XjDFt", "Talia", "warm soft guide"),
|
|
2628
|
+
voice("l7kNoIfnJKPg7779LI2t", "Eddie", "helpful and comforting"),
|
|
2629
|
+
voice("AaOhDHYJ1XLZk74lXhdE", "Caleb", "trusted guide"),
|
|
2630
|
+
voice("gOupLcAkjEnguROwi4oS", "Darian", "warm grounded storyteller"),
|
|
2631
|
+
voice("BFd5oBc2DDna33pSi4Gf", "Alicia", "polished global anchor"),
|
|
2632
|
+
voice("QtY3JBOUKEB5xzrRfOKc", "Maisie", "friendly casual neighbor"),
|
|
2633
|
+
voice("FrS6cKLB1wg4WYgPa9GW", "Wyatt", "seasoned mentor"),
|
|
2634
|
+
voice("6WwXjDDEMyNmFG95zycZ", "Eldrin", "crisp baritone", "british")
|
|
2635
|
+
];
|
|
2636
|
+
var DEFAULT_VOICE = {
|
|
2637
|
+
provider: "off",
|
|
2638
|
+
voiceId: RECOMMENDED_VOICES[0].id,
|
|
2639
|
+
voiceName: RECOMMENDED_VOICES[0].name,
|
|
2640
|
+
model: DEFAULT_VOICE_MODEL,
|
|
2641
|
+
speakOffice: false
|
|
2642
|
+
};
|
|
2643
|
+
var VOICE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
2644
|
+
var MODEL_ID = /^[a-z0-9_.-]{1,64}$/;
|
|
2645
|
+
function voiceSettings(base, patch) {
|
|
2646
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
2647
|
+
const out = { ...base };
|
|
2648
|
+
if (p.provider === "off" || p.provider === "browser" || p.provider === "elevenlabs") out.provider = p.provider;
|
|
2649
|
+
if (typeof p.voiceId === "string" && p.voiceId.trim() && p.voiceId.length <= 200) out.voiceId = p.voiceId.trim();
|
|
2650
|
+
if (typeof p.voiceName === "string") out.voiceName = p.voiceName.trim().slice(0, 80);
|
|
2651
|
+
if (typeof p.model === "string" && MODEL_ID.test(p.model.trim())) out.model = p.model.trim();
|
|
2652
|
+
if (typeof p.speakOffice === "boolean") out.speakOffice = p.speakOffice;
|
|
2653
|
+
return out;
|
|
2654
|
+
}
|
|
2655
|
+
function cacheToPrune(files, now2, max = CACHE_MAX_FILES, maxAgeMs = CACHE_MAX_AGE_MS) {
|
|
2656
|
+
const fresh = files.filter((f) => now2 - f.mtimeMs <= maxAgeMs).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2657
|
+
const keep = new Set(fresh.slice(0, max).map((f) => f.name));
|
|
2658
|
+
return files.filter((f) => !keep.has(f.name)).map((f) => f.name);
|
|
2659
|
+
}
|
|
2660
|
+
function speaks(m, s2) {
|
|
2661
|
+
return m.from === "ceo" || m.from === "office" && s2.speakOffice;
|
|
2662
|
+
}
|
|
2663
|
+
var VoiceApiError = class extends Error {
|
|
2664
|
+
constructor(status, message) {
|
|
2665
|
+
super(message);
|
|
2666
|
+
this.status = status;
|
|
2667
|
+
}
|
|
2668
|
+
status;
|
|
2669
|
+
};
|
|
2670
|
+
var Voice = class {
|
|
2671
|
+
// secrets-file writes, one after another
|
|
2672
|
+
constructor(deps) {
|
|
2673
|
+
this.deps = deps;
|
|
2674
|
+
}
|
|
2675
|
+
deps;
|
|
2676
|
+
key = "";
|
|
2677
|
+
rejected = false;
|
|
2678
|
+
// ElevenLabs said 401: no more calls until the key changes
|
|
2679
|
+
voicesCache = null;
|
|
2680
|
+
making = /* @__PURE__ */ new Map();
|
|
2681
|
+
queue = Promise.resolve();
|
|
2682
|
+
writing = Promise.resolve();
|
|
2683
|
+
async init() {
|
|
2684
|
+
const s2 = await this.readSecrets();
|
|
2685
|
+
this.key = typeof s2.elevenlabsKey === "string" ? s2.elevenlabsKey : "";
|
|
2686
|
+
this.rejected = !!this.key && s2.elevenlabsKeyRejected === true;
|
|
2687
|
+
await this.prune().catch(() => void 0);
|
|
2688
|
+
}
|
|
2689
|
+
keyView() {
|
|
2690
|
+
return { voiceKeySet: !!this.key, voiceKeyHint: this.key.slice(-4) };
|
|
2691
|
+
}
|
|
2692
|
+
/** PUT /api/voice/key: '' removes the key; anything else must pass one cheap ElevenLabs call first. */
|
|
2693
|
+
async setKey(raw) {
|
|
2694
|
+
const key = raw.trim();
|
|
2695
|
+
if (key) {
|
|
2696
|
+
if (key.length > 200 || /\s/.test(key)) throw new HttpError(400, "That doesn't look like an ElevenLabs API key.");
|
|
2697
|
+
try {
|
|
2698
|
+
await this.deps.api.checkKey(key);
|
|
2699
|
+
} catch (err) {
|
|
2700
|
+
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.");
|
|
2701
|
+
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.");
|
|
2702
|
+
throw new HttpError(502, `Couldn't check the key with ElevenLabs: ${err.message}`);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
this.key = key;
|
|
2706
|
+
this.rejected = false;
|
|
2707
|
+
this.voicesCache = null;
|
|
2708
|
+
await this.writeSecrets({ elevenlabsKey: key || void 0, elevenlabsKeyRejected: void 0 });
|
|
2709
|
+
const view2 = this.keyView();
|
|
2710
|
+
this.deps.keyChanged(view2);
|
|
2711
|
+
return view2;
|
|
2712
|
+
}
|
|
2713
|
+
/** GET /api/voice/voices: the account's voices plus the shortlist, shortlist first. Cached for 10 minutes. */
|
|
2714
|
+
async voices() {
|
|
2715
|
+
this.requireKey();
|
|
2716
|
+
const now2 = this.now();
|
|
2717
|
+
if (this.voicesCache && now2 - this.voicesCache.at < VOICES_TTL_MS) return this.voicesCache.list;
|
|
2718
|
+
const key = this.key;
|
|
2719
|
+
const mine = await this.deps.api.listVoices(key).catch((err) => this.failed(err, key, "list the voices"));
|
|
2720
|
+
const recommended = new Map(RECOMMENDED_VOICES.map((v) => [v.id, v]));
|
|
2721
|
+
const list = [
|
|
2722
|
+
...RECOMMENDED_VOICES.map((r) => {
|
|
2723
|
+
const own = mine.find((v) => v.id === r.id);
|
|
2724
|
+
return own ? { ...own, labels: fill(own.labels, r.labels), recommended: true } : r;
|
|
2725
|
+
}),
|
|
2726
|
+
...mine.filter((v) => !recommended.has(v.id)).map((v) => ({ ...v, recommended: false })).sort((a, b) => a.name.localeCompare(b.name))
|
|
2727
|
+
];
|
|
2728
|
+
if (this.key === key) this.voicesCache = { at: now2, list };
|
|
2729
|
+
return list;
|
|
2730
|
+
}
|
|
2731
|
+
/** GET /api/voice/messages/:id: a phone message as mp3, made once and cached. */
|
|
2732
|
+
async messageAudio(id) {
|
|
2733
|
+
const s2 = this.deps.settings();
|
|
2734
|
+
if (s2.provider !== "elevenlabs") throw new HttpError(404, "Messages aren't read by ElevenLabs right now.");
|
|
2735
|
+
const m = this.deps.message(id);
|
|
2736
|
+
if (!m || !speaks(m, s2)) throw new HttpError(404, `No spoken message ${id}`);
|
|
2737
|
+
const text = speechText(m.text);
|
|
2738
|
+
if (!text) throw new HttpError(404, `Message ${id} has nothing to say aloud`);
|
|
2739
|
+
return this.audio(String(m.id), s2.voiceId, s2.model, text);
|
|
2740
|
+
}
|
|
2741
|
+
/** GET /api/voice/sample: the settings' Test button, in `voiceId` (default: the chosen voice). */
|
|
2742
|
+
async sampleAudio(voiceId) {
|
|
2743
|
+
const s2 = this.deps.settings();
|
|
2744
|
+
return this.audio("sample", voiceId || s2.voiceId, s2.model, SAMPLE_LINE);
|
|
2745
|
+
}
|
|
2746
|
+
async audio(name, voiceId, model, text) {
|
|
2747
|
+
if (!VOICE_ID.test(voiceId)) throw new HttpError(400, "Pick an ElevenLabs voice first.");
|
|
2748
|
+
const hash = crypto5.createHash("sha256").update(`${voiceId}
|
|
2749
|
+
${model}
|
|
2750
|
+
${text}`).digest("hex").slice(0, 12);
|
|
2751
|
+
const file = path12.join(this.deps.cacheDir, `${name}-${hash}.mp3`);
|
|
2752
|
+
const cached = await fs11.readFile(file).catch(() => null);
|
|
2753
|
+
if (cached) return cached;
|
|
2754
|
+
this.requireKey();
|
|
2755
|
+
const busy = this.making.get(file);
|
|
2756
|
+
if (busy) return busy;
|
|
2757
|
+
const made = this.oneAtATime(() => this.synthesize(file, { voiceId, model, text })).finally(() => this.making.delete(file));
|
|
2758
|
+
this.making.set(file, made);
|
|
2759
|
+
return made;
|
|
2760
|
+
}
|
|
2761
|
+
oneAtATime(fn) {
|
|
2762
|
+
const run2 = this.queue.then(fn, fn);
|
|
2763
|
+
this.queue = run2.catch(() => void 0);
|
|
2764
|
+
return run2;
|
|
2765
|
+
}
|
|
2766
|
+
async synthesize(file, req) {
|
|
2767
|
+
this.requireKey();
|
|
2768
|
+
const key = this.key;
|
|
2769
|
+
const audio = await this.deps.api.synthesize(key, req, AbortSignal.timeout(SYNTH_TIMEOUT_MS)).catch((err) => this.failed(err, key, "speak the message"));
|
|
2770
|
+
await fs11.mkdir(this.deps.cacheDir, { recursive: true });
|
|
2771
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2772
|
+
await fs11.writeFile(tmp, audio);
|
|
2773
|
+
await fs11.rename(tmp, file);
|
|
2774
|
+
await this.prune().catch(() => void 0);
|
|
2775
|
+
return audio;
|
|
2776
|
+
}
|
|
2777
|
+
/** An ElevenLabs failure as a short 502. A 401 also stops every call until the key changes, and says so once. */
|
|
2778
|
+
async failed(err, key, what) {
|
|
2779
|
+
if (err instanceof VoiceApiError && err.status === 401 && key === this.key && !this.rejected) {
|
|
2780
|
+
this.rejected = true;
|
|
2781
|
+
this.deps.officeNote(KEY_REJECTED_NOTE);
|
|
2782
|
+
await this.writeSecrets({ elevenlabsKeyRejected: true }).catch(() => void 0);
|
|
2783
|
+
}
|
|
2784
|
+
if (err instanceof VoiceApiError && err.status === 401) throw new HttpError(502, "ElevenLabs rejected the key.");
|
|
2785
|
+
throw new HttpError(502, `ElevenLabs couldn't ${what}: ${err.message}`.slice(0, 240));
|
|
2786
|
+
}
|
|
2787
|
+
requireKey() {
|
|
2788
|
+
if (!this.key) throw new HttpError(409, "Add an ElevenLabs API key first.");
|
|
2789
|
+
if (this.rejected) throw new HttpError(502, "ElevenLabs rejected the key. Set a new one in the voice settings.");
|
|
2790
|
+
}
|
|
2791
|
+
async prune() {
|
|
2792
|
+
const names = (await fs11.readdir(this.deps.cacheDir).catch(() => [])).filter((n) => n.endsWith(".mp3"));
|
|
2793
|
+
const files = await Promise.all(names.map(async (name) => ({ name, mtimeMs: (await fs11.stat(path12.join(this.deps.cacheDir, name)).catch(() => null))?.mtimeMs ?? 0 })));
|
|
2794
|
+
for (const name of cacheToPrune(files, this.now())) await fs11.rm(path12.join(this.deps.cacheDir, name), { force: true, maxRetries: 3 });
|
|
2795
|
+
}
|
|
2796
|
+
now() {
|
|
2797
|
+
return this.deps.now?.() ?? Date.now();
|
|
2798
|
+
}
|
|
2799
|
+
async readSecrets() {
|
|
2800
|
+
try {
|
|
2801
|
+
const s2 = JSON.parse(await fs11.readFile(this.deps.secretsFile, "utf8"));
|
|
2802
|
+
return s2 && typeof s2 === "object" ? s2 : {};
|
|
2803
|
+
} catch {
|
|
2804
|
+
return {};
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
/** Merge into the secrets file (other secrets stay), owner-only where the OS has file modes. */
|
|
2808
|
+
writeSecrets(patch) {
|
|
2809
|
+
const write = this.writing.then(() => this.mergeSecrets(patch));
|
|
2810
|
+
this.writing = write.catch(() => void 0);
|
|
2811
|
+
return write;
|
|
2812
|
+
}
|
|
2813
|
+
async mergeSecrets(patch) {
|
|
2814
|
+
const next = { ...await this.readSecrets(), ...patch };
|
|
2815
|
+
for (const k of Object.keys(next)) if (next[k] === void 0) delete next[k];
|
|
2816
|
+
await fs11.mkdir(path12.dirname(this.deps.secretsFile), { recursive: true });
|
|
2817
|
+
const tmp = `${this.deps.secretsFile}.tmp`;
|
|
2818
|
+
await fs11.writeFile(tmp, JSON.stringify(next, null, 2), { mode: 384 });
|
|
2819
|
+
await fs11.rename(tmp, this.deps.secretsFile);
|
|
2820
|
+
await fs11.chmod(this.deps.secretsFile, 384).catch(() => void 0);
|
|
2821
|
+
}
|
|
2822
|
+
};
|
|
2823
|
+
function fill(own, ours) {
|
|
2824
|
+
return { ...own, description: own.description || ours.description };
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
// server/elevenlabs.ts
|
|
2828
|
+
var API = "https://api.elevenlabs.io";
|
|
2829
|
+
var CHECK_TIMEOUT_MS = 1e4;
|
|
2830
|
+
var MAX_VOICE_PAGES = 5;
|
|
2831
|
+
async function call(key, pathname, init, signal) {
|
|
2832
|
+
let res;
|
|
2833
|
+
try {
|
|
2834
|
+
res = await fetch(`${API}${pathname}`, { ...init, signal, headers: { ...init.headers, "xi-api-key": key } });
|
|
2835
|
+
} catch (err) {
|
|
2836
|
+
throw new VoiceApiError(0, signal.aborted ? "it took too long to answer" : `it couldn't be reached (${err.message})`);
|
|
2837
|
+
}
|
|
2838
|
+
if (!res.ok) throw new VoiceApiError(res.status, await reason(res));
|
|
2839
|
+
return res;
|
|
2840
|
+
}
|
|
2841
|
+
async function reason(res) {
|
|
2842
|
+
const body2 = await res.text().catch(() => "");
|
|
2843
|
+
let text = "";
|
|
2844
|
+
try {
|
|
2845
|
+
const d = JSON.parse(body2).detail;
|
|
2846
|
+
if (typeof d === "string") text = d;
|
|
2847
|
+
else if (Array.isArray(d)) text = d.map((x) => x?.msg).filter(Boolean).join("; ");
|
|
2848
|
+
else if (d && typeof d === "object") text = String(d.message ?? d.status ?? "");
|
|
2849
|
+
} catch {
|
|
2850
|
+
}
|
|
2851
|
+
return `${res.status}${text ? `: ${text}` : ""}`.slice(0, 200);
|
|
2852
|
+
}
|
|
2853
|
+
var s = (v) => typeof v === "string" ? v : "";
|
|
2854
|
+
function voiceOption(v) {
|
|
2855
|
+
const l = v.labels ?? {};
|
|
2856
|
+
return {
|
|
2857
|
+
id: s(v.voice_id),
|
|
2858
|
+
name: s(v.name),
|
|
2859
|
+
category: s(v.category),
|
|
2860
|
+
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"]) },
|
|
2861
|
+
previewUrl: s(v.preview_url) || null
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
var elevenLabs = {
|
|
2865
|
+
// The cheapest call a voices-reading key can make: one voice.
|
|
2866
|
+
async checkKey(key) {
|
|
2867
|
+
await call(key, "/v2/voices?page_size=1&include_total_count=false", {}, AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2868
|
+
},
|
|
2869
|
+
async listVoices(key) {
|
|
2870
|
+
const out = [];
|
|
2871
|
+
let token = "";
|
|
2872
|
+
for (let page = 0; page < MAX_VOICE_PAGES; page++) {
|
|
2873
|
+
const q = new URLSearchParams({ page_size: "100", include_total_count: "false", ...token ? { next_page_token: token } : {} });
|
|
2874
|
+
const res = await call(key, `/v2/voices?${q}`, {}, AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2875
|
+
const body2 = await res.json();
|
|
2876
|
+
for (const v of body2.voices ?? []) if (s(v.voice_id)) out.push(voiceOption(v));
|
|
2877
|
+
token = s(body2.next_page_token);
|
|
2878
|
+
if (!body2.has_more || !token) break;
|
|
2879
|
+
}
|
|
2880
|
+
return out;
|
|
2881
|
+
},
|
|
2882
|
+
async synthesize(key, { voiceId, model, text }, signal) {
|
|
2883
|
+
const res = await call(
|
|
2884
|
+
key,
|
|
2885
|
+
`/v1/text-to-speech/${encodeURIComponent(voiceId)}?output_format=mp3_44100_128`,
|
|
2886
|
+
{ method: "POST", headers: { "Content-Type": "application/json", Accept: "audio/mpeg" }, body: JSON.stringify({ text, model_id: model }) },
|
|
2887
|
+
signal
|
|
2888
|
+
);
|
|
2889
|
+
return Buffer.from(await res.arrayBuffer());
|
|
2890
|
+
}
|
|
2891
|
+
};
|
|
2892
|
+
|
|
2454
2893
|
// server/backend.ts
|
|
2455
2894
|
var realBackend = {
|
|
2456
2895
|
demo: false,
|
|
@@ -2465,6 +2904,7 @@ var realBackend = {
|
|
|
2465
2904
|
closeIssue,
|
|
2466
2905
|
mergePull,
|
|
2467
2906
|
updateBranch,
|
|
2907
|
+
rerunFailedJobs,
|
|
2468
2908
|
closePull,
|
|
2469
2909
|
prForBranch,
|
|
2470
2910
|
prDetails,
|
|
@@ -2481,6 +2921,7 @@ var realBackend = {
|
|
|
2481
2921
|
mainDir,
|
|
2482
2922
|
deskDir,
|
|
2483
2923
|
prepareDesk,
|
|
2924
|
+
installDeps: (dir, cb) => installDesk(dir, cb),
|
|
2484
2925
|
removeDesk,
|
|
2485
2926
|
sweepDesks,
|
|
2486
2927
|
releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
|
|
@@ -2491,35 +2932,37 @@ var realBackend = {
|
|
|
2491
2932
|
releaseClis,
|
|
2492
2933
|
detectClis,
|
|
2493
2934
|
previews: realPreviews,
|
|
2494
|
-
office: realOffice
|
|
2935
|
+
office: realOffice,
|
|
2936
|
+
voice: elevenLabs
|
|
2495
2937
|
};
|
|
2496
2938
|
|
|
2497
2939
|
// server/demo.ts
|
|
2498
|
-
import
|
|
2940
|
+
import crypto6 from "node:crypto";
|
|
2499
2941
|
import http from "node:http";
|
|
2500
|
-
import
|
|
2942
|
+
import path13 from "node:path";
|
|
2501
2943
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
2502
|
-
function issue(n, title,
|
|
2503
|
-
return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
2944
|
+
function issue(n, title, body2, fullName, labels = []) {
|
|
2945
|
+
return { number: n, title, body: body2, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
2504
2946
|
}
|
|
2505
2947
|
var repos = /* @__PURE__ */ new Map();
|
|
2506
2948
|
var bareRepos = /* @__PURE__ */ new Set();
|
|
2507
2949
|
var mergedSinceSync = /* @__PURE__ */ new Map();
|
|
2508
2950
|
var closedIssues = /* @__PURE__ */ new Set();
|
|
2509
|
-
var fakeSha = () =>
|
|
2951
|
+
var fakeSha = () => crypto6.randomBytes(20).toString("hex");
|
|
2952
|
+
var runSeq = 1e3;
|
|
2510
2953
|
function runChecks(pr, fail = Math.random() < 0.2) {
|
|
2511
2954
|
Object.assign(pr, { checks: "pending", pendingChecks: ["CI / build", "Vercel"], failedChecks: [] });
|
|
2512
2955
|
setTimeout(() => {
|
|
2513
2956
|
Object.assign(pr, {
|
|
2514
2957
|
checks: fail ? "failing" : "passing",
|
|
2515
2958
|
pendingChecks: [],
|
|
2516
|
-
failedChecks: fail ? [{ name: "CI / build", url: `${pr.url}/
|
|
2959
|
+
failedChecks: fail ? [{ name: "CI / build", url: `${pr.url.replace(/\/pull\/\d+$/, "")}/actions/runs/${++runSeq}/job/1` }] : []
|
|
2517
2960
|
});
|
|
2518
2961
|
}, 12e3 + Math.random() * 1e4);
|
|
2519
2962
|
}
|
|
2520
2963
|
function seed(fullName, description, titles) {
|
|
2521
2964
|
const r = { fullName, description, issues: [], pulls: [], nextNumber: 1 };
|
|
2522
|
-
for (const [title,
|
|
2965
|
+
for (const [title, body2] of titles) r.issues.push(issue(r.nextNumber++, title, body2, fullName));
|
|
2523
2966
|
repos.set(fullName, r);
|
|
2524
2967
|
}
|
|
2525
2968
|
seed("demo-co/pixel-todo", "A cheerful todo app", [
|
|
@@ -2536,7 +2979,7 @@ seed("demo-co/weather-api", "Tiny weather REST API", [
|
|
|
2536
2979
|
["OpenAPI spec", "Publish an OpenAPI 3.1 document at /openapi.json."]
|
|
2537
2980
|
]);
|
|
2538
2981
|
function screenshotSvg(title, url, hue) {
|
|
2539
|
-
const safe = (
|
|
2982
|
+
const safe = (s2) => s2.replace(/[<>&"]/g, "");
|
|
2540
2983
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="400" viewBox="0 0 640 400">
|
|
2541
2984
|
<rect width="640" height="400" fill="hsl(${hue},60%,97%)"/>
|
|
2542
2985
|
<rect width="640" height="56" fill="hsl(${hue},70%,55%)"/>
|
|
@@ -2550,7 +2993,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
2550
2993
|
</svg>`;
|
|
2551
2994
|
}
|
|
2552
2995
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
2553
|
-
const branch =
|
|
2996
|
+
const branch = path13.basename(opts.cwd);
|
|
2554
2997
|
const hue = issueNumber * 67 % 360;
|
|
2555
2998
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
2556
2999
|
const port = 5200 + issueNumber % 50;
|
|
@@ -2641,8 +3084,8 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2641
3084
|
{ kind: "system", text: ` cwd ${opts.cwd}` }
|
|
2642
3085
|
];
|
|
2643
3086
|
const checks = opts.prompt.match(/^GitHub checks right now: .*$/m)?.[0] ?? "GitHub checks right now: none";
|
|
2644
|
-
const
|
|
2645
|
-
const script = [header, ...
|
|
3087
|
+
const body2 = kind === "qa" ? qaScript(cb, number, title, round, checks) : kind === "fix" ? fixScript(number, pushes) : devScript(opts, cb, number, title);
|
|
3088
|
+
const script = [header, ...body2];
|
|
2646
3089
|
const finish = () => {
|
|
2647
3090
|
const costUsd = 0.3 + Math.random();
|
|
2648
3091
|
const turns = 15 + Math.floor(Math.random() * 20);
|
|
@@ -2697,7 +3140,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2697
3140
|
number: n,
|
|
2698
3141
|
title,
|
|
2699
3142
|
url,
|
|
2700
|
-
headRefName: `swarm/issue-${number}-${
|
|
3143
|
+
headRefName: `swarm/issue-${number}-${path13.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
2701
3144
|
state: "OPEN",
|
|
2702
3145
|
isDraft: false,
|
|
2703
3146
|
mergeable: "MERGEABLE",
|
|
@@ -2723,12 +3166,12 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2723
3166
|
const step = () => {
|
|
2724
3167
|
if (stopped) return;
|
|
2725
3168
|
if (i >= script.length) return finish();
|
|
2726
|
-
const
|
|
2727
|
-
if (typeof
|
|
3169
|
+
const s2 = script[i++];
|
|
3170
|
+
if (typeof s2 === "function") s2();
|
|
2728
3171
|
else {
|
|
2729
|
-
const tool2 =
|
|
3172
|
+
const tool2 = s2.find((e) => e.kind === "tool");
|
|
2730
3173
|
cb.tool(tool2?.tool ?? null);
|
|
2731
|
-
cb.log(
|
|
3174
|
+
cb.log(s2);
|
|
2732
3175
|
}
|
|
2733
3176
|
timers.push(setTimeout(step, 1600 + Math.random() * 3800));
|
|
2734
3177
|
};
|
|
@@ -2826,6 +3269,7 @@ function fakeUsageWarning(cb) {
|
|
|
2826
3269
|
}
|
|
2827
3270
|
function createDemoBackend() {
|
|
2828
3271
|
const deskRepo = /* @__PURE__ */ new Map();
|
|
3272
|
+
const installedDesks = /* @__PURE__ */ new Set();
|
|
2829
3273
|
const folders = /* @__PURE__ */ new Map();
|
|
2830
3274
|
const addFolder = (name, github, git2 = true) => folders.set(name, { name, path: `/demo/projects/${name}`, git: git2, github, modified: Date.now() - folders.size * 36e5 });
|
|
2831
3275
|
for (const r of repos.values()) addFolder(r.fullName.split("/")[1], r.fullName);
|
|
@@ -2867,11 +3311,11 @@ function createDemoBackend() {
|
|
|
2867
3311
|
},
|
|
2868
3312
|
listIssues: async (fullName) => [...repos.get(fullName)?.issues ?? []],
|
|
2869
3313
|
listPulls: async (fullName) => [...repos.get(fullName)?.pulls ?? []],
|
|
2870
|
-
createIssue: async (fullName, title,
|
|
3314
|
+
createIssue: async (fullName, title, body2, labels = []) => {
|
|
2871
3315
|
const r = repos.get(fullName);
|
|
2872
3316
|
if (!r) throw new Error("Unknown repo");
|
|
2873
3317
|
const n = r.nextNumber++;
|
|
2874
|
-
r.issues.push(issue(n, title,
|
|
3318
|
+
r.issues.push(issue(n, title, body2, fullName, labels));
|
|
2875
3319
|
return n;
|
|
2876
3320
|
},
|
|
2877
3321
|
issueState: async (fullName, number) => {
|
|
@@ -2909,6 +3353,11 @@ function createDemoBackend() {
|
|
|
2909
3353
|
Object.assign(pr, { headSha: fakeSha(), mergeState: "CLEAN" });
|
|
2910
3354
|
runChecks(pr, false);
|
|
2911
3355
|
},
|
|
3356
|
+
rerunFailedJobs: async (fullName, runIds) => {
|
|
3357
|
+
for (const pr of repos.get(fullName)?.pulls ?? []) {
|
|
3358
|
+
if (pr.failedChecks.some((c) => runIds.some((id) => c.url?.includes(`/actions/runs/${id}/`)))) runChecks(pr, false);
|
|
3359
|
+
}
|
|
3360
|
+
},
|
|
2912
3361
|
closePull: async (fullName, number) => {
|
|
2913
3362
|
const pr = repos.get(fullName)?.pulls.find((p) => p.number === number);
|
|
2914
3363
|
if (pr) pr.state = "CLOSED";
|
|
@@ -2959,6 +3408,18 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2959
3408
|
deskRepo.set(dir, fullName);
|
|
2960
3409
|
return dir;
|
|
2961
3410
|
},
|
|
3411
|
+
installDeps: async (dir, cb) => {
|
|
3412
|
+
if (installedDesks.has(dir)) {
|
|
3413
|
+
cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
3414
|
+
return "skipped";
|
|
3415
|
+
}
|
|
3416
|
+
cb.log(["Installing dependencies\u2026", "$ npm ci"]);
|
|
3417
|
+
cb.installing();
|
|
3418
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
3419
|
+
cb.log(["Dependencies installed."]);
|
|
3420
|
+
installedDesks.add(dir);
|
|
3421
|
+
return "installed";
|
|
3422
|
+
},
|
|
2962
3423
|
removeDesk: async () => void 0,
|
|
2963
3424
|
sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
|
|
2964
3425
|
releaseDesk: async () => void 0,
|
|
@@ -2973,7 +3434,8 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2973
3434
|
releaseClis: async () => void 0,
|
|
2974
3435
|
detectClis: async () => CLIS.map((c) => ({ id: c.id, label: c.label, installed: true, version: "demo", integrated: c.integrated })),
|
|
2975
3436
|
previews: demoPreviews,
|
|
2976
|
-
office: demoOffice
|
|
3437
|
+
office: demoOffice,
|
|
3438
|
+
voice: demoVoice
|
|
2977
3439
|
};
|
|
2978
3440
|
}
|
|
2979
3441
|
var OFFICE_REPO = "demo-co/pixel-todo";
|
|
@@ -3078,11 +3540,11 @@ var demoPreviews = {
|
|
|
3078
3540
|
async stop() {
|
|
3079
3541
|
stopped = true;
|
|
3080
3542
|
timers.forEach(clearTimeout);
|
|
3081
|
-
const
|
|
3543
|
+
const s2 = server2;
|
|
3082
3544
|
server2 = null;
|
|
3083
|
-
if (!
|
|
3084
|
-
|
|
3085
|
-
await new Promise((resolve) =>
|
|
3545
|
+
if (!s2?.listening) return;
|
|
3546
|
+
s2.closeAllConnections();
|
|
3547
|
+
await new Promise((resolve) => s2.close(() => resolve()));
|
|
3086
3548
|
}
|
|
3087
3549
|
};
|
|
3088
3550
|
}
|
|
@@ -3154,12 +3616,12 @@ function ceoSession(opts, cb) {
|
|
|
3154
3616
|
};
|
|
3155
3617
|
const status = async () => {
|
|
3156
3618
|
await step([{ kind: "tool", tool: "mcp__office__company_status", text: `\u23FA ${describeOfficeTool("company_status", {})}` }]);
|
|
3157
|
-
const
|
|
3158
|
-
const people =
|
|
3159
|
-
const issues =
|
|
3160
|
-
const free =
|
|
3161
|
-
cb.log([{ kind: "result", text: ` \u23BF ${
|
|
3162
|
-
return
|
|
3619
|
+
const s2 = JSON.parse(await office.call("company_status", {}));
|
|
3620
|
+
const people = s2.floors.reduce((n, f) => n + f.team.length, 0);
|
|
3621
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3622
|
+
const free = s2.floors.reduce((n, f) => n + f.seats.dev.free + f.seats.qa.free, 0);
|
|
3623
|
+
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` }]);
|
|
3624
|
+
return s2;
|
|
3163
3625
|
};
|
|
3164
3626
|
const use = async (name, args) => {
|
|
3165
3627
|
await step([{ kind: "tool", tool: `mcp__office__${name}`, text: `\u23FA ${describeOfficeTool(name, args)}` }]);
|
|
@@ -3169,7 +3631,7 @@ function ceoSession(opts, cb) {
|
|
|
3169
3631
|
};
|
|
3170
3632
|
const read = (file, lines) => step([{ kind: "tool", tool: "Read", text: `\u23FA Read ${file}` }, { kind: "result", text: ` \u23BF Read ${lines} lines` }]);
|
|
3171
3633
|
const think = (text) => step([{ kind: "thinking", text: "\u273B Thinking\u2026" }, { kind: "text", text: `\u25CF ${text}` }], 1800);
|
|
3172
|
-
const short = (
|
|
3634
|
+
const short = (s2, n = 48) => s2.length > n ? `${s2.slice(0, n - 1).trim()}\u2026` : s2;
|
|
3173
3635
|
const planIssues = async (floor, mission, team) => {
|
|
3174
3636
|
const first = await use("file_issue", {
|
|
3175
3637
|
floor,
|
|
@@ -3212,8 +3674,8 @@ Acceptance criteria:
|
|
|
3212
3674
|
};
|
|
3213
3675
|
const scripts = {
|
|
3214
3676
|
async onboard(floor, fullName) {
|
|
3215
|
-
const
|
|
3216
|
-
const f =
|
|
3677
|
+
const s2 = await status();
|
|
3678
|
+
const f = s2.floors.find((x) => x.floor === floor);
|
|
3217
3679
|
if (!f) return "That floor has gone, so there was nothing to onboard.";
|
|
3218
3680
|
const p = PROFILES[fullName.split("/")[1] ?? ""] ?? GENERIC;
|
|
3219
3681
|
await read(`${f.repo}/README.md`, 48);
|
|
@@ -3245,8 +3707,8 @@ Acceptance criteria:
|
|
|
3245
3707
|
].filter(Boolean).join(" ");
|
|
3246
3708
|
},
|
|
3247
3709
|
async plan(floor, mission) {
|
|
3248
|
-
const
|
|
3249
|
-
const f =
|
|
3710
|
+
const s2 = await status();
|
|
3711
|
+
const f = s2.floors.find((x) => x.floor === floor);
|
|
3250
3712
|
if (!f) return "That floor has gone, so there was nothing to plan.";
|
|
3251
3713
|
await read(`${f.repo}/README.md`, 12);
|
|
3252
3714
|
await think("Foundation first, so the parallel work doesn't collide.");
|
|
@@ -3254,9 +3716,9 @@ Acceptance criteria:
|
|
|
3254
3716
|
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.`;
|
|
3255
3717
|
},
|
|
3256
3718
|
async review() {
|
|
3257
|
-
const
|
|
3719
|
+
const s2 = await status();
|
|
3258
3720
|
await think("Checking each floor for idle people and stuck work.");
|
|
3259
|
-
for (const f of
|
|
3721
|
+
for (const f of s2.floors) {
|
|
3260
3722
|
const idle = f.team.filter((a) => a.role === "dev" && !a.specialty && (a.status === "idle" || a.status === "done"));
|
|
3261
3723
|
const devs = f.team.filter((a) => a.role === "dev").length;
|
|
3262
3724
|
if (devs >= 6 && idle.length >= 2 && f.backlog.length < devs) {
|
|
@@ -3264,26 +3726,33 @@ Acceptance criteria:
|
|
|
3264
3726
|
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.`;
|
|
3265
3727
|
}
|
|
3266
3728
|
}
|
|
3267
|
-
for (const f of
|
|
3729
|
+
for (const f of s2.floors) {
|
|
3268
3730
|
const specialist = f.team.find((a) => a.role === "dev" && a.specialty);
|
|
3269
3731
|
const unrouted = f.backlog.find((i) => !i.specialty && !i.inProgress);
|
|
3270
3732
|
if (!specialist || !unrouted) continue;
|
|
3271
3733
|
const out = await use("route_issue", { floor: f.floor, number: unrouted.number, specialty: specialist.specialty });
|
|
3272
3734
|
if (!out.startsWith("Refused")) return `Floor ${f.floor}: #${unrouted.number} had no specialty, so I routed it to ${specialist.specialty}, ${specialist.name}'s lane.`;
|
|
3273
3735
|
}
|
|
3274
|
-
const issues =
|
|
3275
|
-
const prs =
|
|
3276
|
-
return `All ${
|
|
3736
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3737
|
+
const prs = s2.floors.reduce((n, f) => n + f.pullRequests.length, 0);
|
|
3738
|
+
return `All ${s2.floors.length} floors look healthy: ${issues} open issues and ${prs} pull requests in flight. No changes needed.`;
|
|
3277
3739
|
},
|
|
3278
3740
|
async chat(text) {
|
|
3279
|
-
const
|
|
3741
|
+
const s2 = await status();
|
|
3280
3742
|
await think("Reading your message.");
|
|
3281
|
-
const
|
|
3282
|
-
const
|
|
3283
|
-
|
|
3284
|
-
|
|
3743
|
+
const close = text.match(/\bclose #(\d+)[\s,:;.-]*(.*)/i);
|
|
3744
|
+
const closeFloor = close && (s2.floors.find((f) => f.backlog.some((i) => i.number === Number(close[1]))) ?? s2.floors[0]);
|
|
3745
|
+
if (close && closeFloor) {
|
|
3746
|
+
const reason2 = close[2].trim() || "No longer wanted.";
|
|
3747
|
+
const out = await use("close_issue", { floor: closeFloor.floor, number: Number(close[1]), reason: reason2 });
|
|
3748
|
+
return out.startsWith("Refused") ? `I couldn't close #${close[1]}: ${out.replace(/^Refused: /, "")}` : `Done: ${out} I left "${short(reason2, 80)}" on it as a comment.`;
|
|
3749
|
+
}
|
|
3750
|
+
const people = s2.floors.reduce((n, f) => n + f.team.length, 0);
|
|
3751
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3752
|
+
const pending2 = s2.pendingProposals.length;
|
|
3753
|
+
const rows = s2.floors.map((f) => `| ${f.floor} | \`${f.repo.split("/").pop()}\` | ${f.team.length} | ${f.backlog.length} | ${f.pullRequests.length} |`);
|
|
3285
3754
|
return [
|
|
3286
|
-
`**Quick status:** ${
|
|
3755
|
+
`**Quick status:** ${s2.floors.length} floor${s2.floors.length === 1 ? "" : "s"}, ${people} people and ${issues} open issues.`,
|
|
3287
3756
|
"",
|
|
3288
3757
|
"- The team is *heads down* on the backlog",
|
|
3289
3758
|
`- ${pending2 ? `**${pending2}** proposal${pending2 === 1 ? " is" : "s are"} waiting for you in Hires` : "No hiring decisions waiting on you"}`,
|
|
@@ -3347,34 +3816,164 @@ Acceptance criteria:
|
|
|
3347
3816
|
}
|
|
3348
3817
|
};
|
|
3349
3818
|
}
|
|
3819
|
+
function demoChime(chars) {
|
|
3820
|
+
const rate = 8e3;
|
|
3821
|
+
const seconds = Math.min(0.4 + chars / 400, 2);
|
|
3822
|
+
const n = Math.round(rate * seconds);
|
|
3823
|
+
const wav = Buffer.alloc(44 + n);
|
|
3824
|
+
wav.write("RIFF", 0);
|
|
3825
|
+
wav.writeUInt32LE(36 + n, 4);
|
|
3826
|
+
wav.write("WAVEfmt ", 8);
|
|
3827
|
+
wav.writeUInt32LE(16, 16);
|
|
3828
|
+
wav.writeUInt16LE(1, 20);
|
|
3829
|
+
wav.writeUInt16LE(1, 22);
|
|
3830
|
+
wav.writeUInt32LE(rate, 24);
|
|
3831
|
+
wav.writeUInt32LE(rate, 28);
|
|
3832
|
+
wav.writeUInt16LE(1, 32);
|
|
3833
|
+
wav.writeUInt16LE(8, 34);
|
|
3834
|
+
wav.write("data", 36);
|
|
3835
|
+
wav.writeUInt32LE(n, 40);
|
|
3836
|
+
for (let i = 0; i < n; i++) {
|
|
3837
|
+
const t = i / rate;
|
|
3838
|
+
const f = t < seconds / 2 ? 660 : 880;
|
|
3839
|
+
const fade = Math.min(1, (seconds - t) * 8, t * 40);
|
|
3840
|
+
wav[44 + i] = Math.round(128 + 40 * fade * Math.sin(2 * Math.PI * f * t));
|
|
3841
|
+
}
|
|
3842
|
+
return wav;
|
|
3843
|
+
}
|
|
3844
|
+
var demoVoices = [
|
|
3845
|
+
{ id: "demoVoiceAvery00001", name: "Avery", category: "premade", labels: { accent: "american", gender: "female", age: "middle aged", description: "calm", use_case: "conversational" }, previewUrl: null },
|
|
3846
|
+
{ id: "demoVoiceBasil00002", name: "Basil", category: "premade", labels: { accent: "british", gender: "male", age: "middle aged", description: "warm", use_case: "narration" }, previewUrl: null },
|
|
3847
|
+
{ id: "demoVoiceCleo000003", name: "Cleo", category: "premade", labels: { accent: "australian", gender: "female", age: "young", description: "friendly", use_case: "conversational" }, previewUrl: null }
|
|
3848
|
+
];
|
|
3849
|
+
var demoVoice = {
|
|
3850
|
+
checkKey: async (key) => {
|
|
3851
|
+
if (/bad/i.test(key)) throw new VoiceApiError(401, "401: invalid_api_key (demo)");
|
|
3852
|
+
},
|
|
3853
|
+
listVoices: async () => demoVoices,
|
|
3854
|
+
synthesize: async (_key, { text }) => {
|
|
3855
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3856
|
+
return demoChime(text.length);
|
|
3857
|
+
}
|
|
3858
|
+
};
|
|
3859
|
+
|
|
3860
|
+
// server/screenReply.ts
|
|
3861
|
+
function screenStatus(shot) {
|
|
3862
|
+
if (shot === void 0) return 404;
|
|
3863
|
+
return shot ? 200 : 204;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
// server/sendBack.ts
|
|
3867
|
+
import { z } from "zod";
|
|
3868
|
+
|
|
3869
|
+
// server/mergeGate.ts
|
|
3870
|
+
var MAX_MERGE_FIXES = 3;
|
|
3871
|
+
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
3872
|
+
var MERGE_RETRY_MS = 10 * 6e4;
|
|
3873
|
+
var RERUN_GRACE_MS = 2 * 6e4;
|
|
3874
|
+
function mergeStep(pr, rec, now2, opts) {
|
|
3875
|
+
if (pr.isDraft) return { do: "wait", note: "a draft: waiting", set: {} };
|
|
3876
|
+
if (!opts.detailed && (pr.mergeable === "UNKNOWN" || pr.mergeState === "UNKNOWN")) return { do: "details", set: {} };
|
|
3877
|
+
const set = {};
|
|
3878
|
+
if (rec.passedSha == null) set.passedSha = pr.headSha;
|
|
3879
|
+
if (pr.headSha !== (set.passedSha ?? rec.passedSha)) return { do: "requeue", set };
|
|
3880
|
+
const needsHuman = rec.mergeFixes >= MAX_MERGE_FIXES;
|
|
3881
|
+
if (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY") {
|
|
3882
|
+
return { do: "send-back", reason: "conflict", instructions: `It conflicts with ${opts.base}.`, needsHuman, set };
|
|
3883
|
+
}
|
|
3884
|
+
const rerun = rec.rerunSha === pr.headSha;
|
|
3885
|
+
if (pr.checks === "failing") {
|
|
3886
|
+
const instructions = pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n");
|
|
3887
|
+
const runIds = failedRunIds(pr.failedChecks);
|
|
3888
|
+
if (!rerun && runIds.length) return { do: "rerun", runIds, reason: "checks", instructions, needsHuman, set };
|
|
3889
|
+
if (rerun && rec.rerunAt != null && now2 - rec.rerunAt < RERUN_GRACE_MS) return { do: "wait", note: "re-running a failed check", set };
|
|
3890
|
+
return { do: "send-back", reason: "checks", instructions, needsHuman, set };
|
|
3891
|
+
}
|
|
3892
|
+
if (pr.checks === "pending") {
|
|
3893
|
+
const since = rec.pendingSince ?? now2;
|
|
3894
|
+
if (rec.pendingSince == null) set.pendingSince = since;
|
|
3895
|
+
const alert = now2 - since > CHECKS_ALERT_MS && !rec.alerted;
|
|
3896
|
+
if (alert) set.alerted = true;
|
|
3897
|
+
const running = pr.pendingChecks.join(", ") || "running";
|
|
3898
|
+
return { do: "wait", note: rerun ? `re-running a failed check: ${running}` : `waiting for checks: ${running}`, alert, set };
|
|
3899
|
+
}
|
|
3900
|
+
set.pendingSince = null;
|
|
3901
|
+
if (pr.mergeable === "UNKNOWN") return { do: "wait", note: "GitHub is checking it can merge", set };
|
|
3902
|
+
if (pr.mergeState === "BEHIND") return { do: "update-branch", set };
|
|
3903
|
+
if (rec.mergeRetryAt && now2 < rec.mergeRetryAt) return { do: "wait", set };
|
|
3904
|
+
return { do: "merge", set };
|
|
3905
|
+
}
|
|
3906
|
+
function failedRunIds(failed2) {
|
|
3907
|
+
const ids = failed2.map((c) => c.url?.match(/\/actions\/runs\/(\d+)/)?.[1]).filter((id) => !!id);
|
|
3908
|
+
return [...new Set(ids)].map(Number);
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
// server/qaOutcome.ts
|
|
3912
|
+
var MAX_QA_ROUNDS = 3;
|
|
3913
|
+
function lastQaRound(rec) {
|
|
3914
|
+
return rec.round - rec.retests >= MAX_QA_ROUNDS;
|
|
3915
|
+
}
|
|
3916
|
+
function qaOutcome(pass, rec, pr) {
|
|
3917
|
+
if (pass) return "passed";
|
|
3918
|
+
if (!lastQaRound(rec)) return "failed";
|
|
3919
|
+
const conflicting = pr != null && (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY");
|
|
3920
|
+
return conflicting && rec.mergeFixes < MAX_MERGE_FIXES ? "conflict" : "needs-human";
|
|
3921
|
+
}
|
|
3922
|
+
function conflictFixInstructions(qaInstructions2, base) {
|
|
3923
|
+
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.`;
|
|
3924
|
+
return qaInstructions2.trim() ? `${qaInstructions2.trim()}
|
|
3925
|
+
|
|
3926
|
+
${merge}` : merge;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
// server/sendBack.ts
|
|
3930
|
+
var MAX_SEND_BACK_NOTE = 1e3;
|
|
3931
|
+
var body = z.object({ note: z.string().max(MAX_SEND_BACK_NOTE).optional() });
|
|
3932
|
+
function parseSendBackNote(input) {
|
|
3933
|
+
const parsed = body.safeParse(input ?? {});
|
|
3934
|
+
if (!parsed.success) throw new HttpError(400, `note must be text of at most ${MAX_SEND_BACK_NOTE} characters`);
|
|
3935
|
+
return parsed.data.note?.trim() || void 0;
|
|
3936
|
+
}
|
|
3937
|
+
function sendBackPatch(prNumber, pr, rec, base, note) {
|
|
3938
|
+
if (!pr || pr.state !== "OPEN") throw new HttpError(404, `PR #${prNumber} is not open`);
|
|
3939
|
+
if (rec?.status === "testing" || rec?.status === "fixing") throw new HttpError(409, `PR #${prNumber} is already ${rec.status}`);
|
|
3940
|
+
if (!rec || rec.status !== "needs-human" && rec.status !== "failed") {
|
|
3941
|
+
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`);
|
|
3942
|
+
}
|
|
3943
|
+
const conflict = pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY";
|
|
3944
|
+
const findings = [rec.fixInstructions?.trim(), note?.trim() && `From the manager: ${note.trim()}`].filter(Boolean).join("\n\n");
|
|
3945
|
+
const addMerge = conflict && !rec.passedSha && !findings.includes(`conflicts with ${base}`);
|
|
3946
|
+
const fixInstructions = (addMerge ? conflictFixInstructions(findings, base) : findings) || null;
|
|
3947
|
+
return { status: "failed", fixReason: conflict ? "conflict" : "qa", fixInstructions, sessionFailures: 0, retests: rec.retests + 1, mergeNote: null };
|
|
3948
|
+
}
|
|
3350
3949
|
|
|
3351
3950
|
// server/swarm.ts
|
|
3352
|
-
import
|
|
3353
|
-
import
|
|
3354
|
-
import
|
|
3951
|
+
import fs13 from "node:fs/promises";
|
|
3952
|
+
import path15 from "node:path";
|
|
3953
|
+
import crypto7 from "node:crypto";
|
|
3355
3954
|
|
|
3356
3955
|
// server/ceo.ts
|
|
3357
3956
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
3358
|
-
import { z } from "zod";
|
|
3957
|
+
import { z as z2 } from "zod";
|
|
3359
3958
|
|
|
3360
3959
|
// shared/issues.ts
|
|
3361
3960
|
function issueSpecialty(labels) {
|
|
3362
3961
|
const l = labels.find((x) => /^swarm:/i.test(x) && !/^swarm:skip$/i.test(x));
|
|
3363
3962
|
return l ? l.slice(6).toLowerCase() : "";
|
|
3364
3963
|
}
|
|
3365
|
-
function blockers(
|
|
3964
|
+
function blockers(body2, open) {
|
|
3366
3965
|
const out = /* @__PURE__ */ new Set();
|
|
3367
|
-
for (const m of (
|
|
3966
|
+
for (const m of (body2 ?? "").matchAll(/\b(?:depends\s+on|blocked\s+by)\s*:?\s*((?:#\d+(?:\s*(?:,|and|&)\s*)?)+)/gi)) {
|
|
3368
3967
|
for (const n of m[1].matchAll(/#(\d+)/g)) if (open.has(Number(n[1]))) out.add(Number(n[1]));
|
|
3369
3968
|
}
|
|
3370
3969
|
return [...out];
|
|
3371
3970
|
}
|
|
3372
3971
|
var DEPENDENCY = /\b(?:depends\s+on|blocked\s+by)\s*:?\s*(?:#\d+(?:\s*(?:,|and|&)\s*)?)+/gi;
|
|
3373
3972
|
var STATEMENT = new RegExp(`\\s*${DEPENDENCY.source}[.;,]?`, "gi");
|
|
3374
|
-
function setDependsOn(
|
|
3973
|
+
function setDependsOn(body2, deps) {
|
|
3375
3974
|
const lines = [];
|
|
3376
3975
|
let dropped = false;
|
|
3377
|
-
for (const line of (
|
|
3976
|
+
for (const line of (body2 ?? "").split(/\r?\n/)) {
|
|
3378
3977
|
const rest2 = line.match(DEPENDENCY) ? line.replace(STATEMENT, "").trimEnd() : line;
|
|
3379
3978
|
if (rest2 !== line && !/[\p{L}\p{N}]/u.test(rest2)) {
|
|
3380
3979
|
dropped = true;
|
|
@@ -3423,7 +4022,7 @@ function holdUps(issues) {
|
|
|
3423
4022
|
}
|
|
3424
4023
|
|
|
3425
4024
|
// server/ceo.ts
|
|
3426
|
-
var EFFORT =
|
|
4025
|
+
var EFFORT = z2.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
3427
4026
|
function createOfficeTools(h) {
|
|
3428
4027
|
const run2 = async (fn) => {
|
|
3429
4028
|
try {
|
|
@@ -3442,21 +4041,21 @@ function createOfficeTools(h) {
|
|
|
3442
4041
|
tool(
|
|
3443
4042
|
"agent_detail",
|
|
3444
4043
|
"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.",
|
|
3445
|
-
{ agent_id:
|
|
4044
|
+
{ agent_id: z2.string().describe("An id (or name) from company_status") },
|
|
3446
4045
|
(a) => run2(() => h.agentDetail(a))
|
|
3447
4046
|
),
|
|
3448
4047
|
tool(
|
|
3449
4048
|
"set_floor_profile",
|
|
3450
4049
|
"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.",
|
|
3451
4050
|
{
|
|
3452
|
-
floor:
|
|
3453
|
-
summary:
|
|
3454
|
-
qa_brief:
|
|
3455
|
-
preview_command:
|
|
4051
|
+
floor: z2.number().int().describe("Floor number"),
|
|
4052
|
+
summary: z2.string().max(140).optional().describe('e.g. "3D browser game \xB7 Three.js + Vite + TypeScript"'),
|
|
4053
|
+
qa_brief: z2.string().max(2500).optional().describe("What QA must check on every PR for this project, as short bullet points"),
|
|
4054
|
+
preview_command: z2.string().max(2e3).optional().describe(
|
|
3456
4055
|
'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.'
|
|
3457
4056
|
),
|
|
3458
4057
|
// Not z.record(): the SDK can't turn it into JSON Schema, and one bad tool empties the whole tools/list.
|
|
3459
|
-
preview_env:
|
|
4058
|
+
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.")
|
|
3460
4059
|
},
|
|
3461
4060
|
(a) => run2(() => h.setFloorProfile(a))
|
|
3462
4061
|
),
|
|
@@ -3464,10 +4063,10 @@ function createOfficeTools(h) {
|
|
|
3464
4063
|
"update_job",
|
|
3465
4064
|
"Change an existing agent's job title, specialty or job description so it fits the project. Takes effect from their next task.",
|
|
3466
4065
|
{
|
|
3467
|
-
agent_id:
|
|
3468
|
-
title:
|
|
3469
|
-
specialty:
|
|
3470
|
-
job_description:
|
|
4066
|
+
agent_id: z2.string(),
|
|
4067
|
+
title: z2.string().max(60).optional(),
|
|
4068
|
+
specialty: z2.string().max(24).optional().describe('Short lowercase slug, e.g. "graphics"; "" for a generalist'),
|
|
4069
|
+
job_description: z2.string().max(2500).optional()
|
|
3471
4070
|
},
|
|
3472
4071
|
(a) => run2(() => h.updateJob(a))
|
|
3473
4072
|
),
|
|
@@ -3475,13 +4074,13 @@ function createOfficeTools(h) {
|
|
|
3475
4074
|
"propose_hire",
|
|
3476
4075
|
"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).",
|
|
3477
4076
|
{
|
|
3478
|
-
floor:
|
|
3479
|
-
role:
|
|
3480
|
-
title:
|
|
3481
|
-
specialty:
|
|
3482
|
-
job_description:
|
|
3483
|
-
reason:
|
|
3484
|
-
model:
|
|
4077
|
+
floor: z2.number().int(),
|
|
4078
|
+
role: z2.enum(["dev", "qa"]).describe("dev = builds issues into pull requests; qa = tests pull requests"),
|
|
4079
|
+
title: z2.string().max(60).describe('Specific job title, e.g. "Three.js graphics engineer"'),
|
|
4080
|
+
specialty: z2.string().max(24).describe('Short lowercase slug used to label issues swarm:<specialty>, e.g. "graphics"'),
|
|
4081
|
+
job_description: z2.string().max(2500).describe("What this person owns on this project and how they should work. Written to them, second person."),
|
|
4082
|
+
reason: z2.string().max(600).describe("Why the floor needs them now. The manager reads this."),
|
|
4083
|
+
model: z2.string().optional().describe("Leave out to use the default model"),
|
|
3485
4084
|
effort: EFFORT.optional().describe("Leave out to use the default effort")
|
|
3486
4085
|
},
|
|
3487
4086
|
(a) => run2(() => h.proposeHire(a))
|
|
@@ -3489,17 +4088,17 @@ function createOfficeTools(h) {
|
|
|
3489
4088
|
tool(
|
|
3490
4089
|
"propose_let_go",
|
|
3491
4090
|
"Propose letting an agent go (overstaffed floor, specialty no longer needed). The manager decides.",
|
|
3492
|
-
{ agent_id:
|
|
4091
|
+
{ agent_id: z2.string(), reason: z2.string().max(600) },
|
|
3493
4092
|
(a) => run2(() => h.proposeLetGo(a))
|
|
3494
4093
|
),
|
|
3495
4094
|
tool(
|
|
3496
4095
|
"file_issue",
|
|
3497
4096
|
'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.',
|
|
3498
4097
|
{
|
|
3499
|
-
floor:
|
|
3500
|
-
title:
|
|
3501
|
-
body:
|
|
3502
|
-
specialty:
|
|
4098
|
+
floor: z2.number().int(),
|
|
4099
|
+
title: z2.string().max(120),
|
|
4100
|
+
body: z2.string().max(6e3).describe("Context, what to build, acceptance criteria"),
|
|
4101
|
+
specialty: z2.string().max(24).optional()
|
|
3503
4102
|
},
|
|
3504
4103
|
(a) => run2(() => h.fileIssue(a))
|
|
3505
4104
|
),
|
|
@@ -3507,12 +4106,22 @@ function createOfficeTools(h) {
|
|
|
3507
4106
|
"route_issue",
|
|
3508
4107
|
'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.',
|
|
3509
4108
|
{
|
|
3510
|
-
floor:
|
|
3511
|
-
number:
|
|
3512
|
-
specialty:
|
|
3513
|
-
depends_on:
|
|
4109
|
+
floor: z2.number().int(),
|
|
4110
|
+
number: z2.number().int().positive().describe("The issue number"),
|
|
4111
|
+
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.'),
|
|
4112
|
+
depends_on: z2.array(z2.number().int().positive()).max(10).optional().describe("Issues it waits for; [] for none. Not for an issue in progress.")
|
|
3514
4113
|
},
|
|
3515
4114
|
(a) => run2(() => h.routeIssue(a))
|
|
4115
|
+
),
|
|
4116
|
+
tool(
|
|
4117
|
+
"close_issue",
|
|
4118
|
+
'Close an open issue that is superseded or no longer wanted, as "not planned", with your reason as a comment. Not while an open pull request closes it. Nothing is deleted.',
|
|
4119
|
+
{
|
|
4120
|
+
floor: z2.number().int(),
|
|
4121
|
+
number: z2.number().int().positive().describe("The issue number"),
|
|
4122
|
+
reason: z2.string().min(1).max(1e3).describe('Posted as a comment, e.g. "Superseded by #152."')
|
|
4123
|
+
},
|
|
4124
|
+
(a) => run2(() => h.closeIssue(a))
|
|
3516
4125
|
)
|
|
3517
4126
|
];
|
|
3518
4127
|
const server2 = createSdkMcpServer({ name: "office", version: "1.0.0", tools: defs });
|
|
@@ -3542,7 +4151,9 @@ function ceoSystemPrompt(o) {
|
|
|
3542
4151
|
"Your job is to run the company, not to write code:",
|
|
3543
4152
|
"- Understand each project: what it is, its stack, how far along it is, and what kind of people it needs. Projects differ a lot. A static marketing site, a 3D browser game and a REST API need different specialists and different QA.",
|
|
3544
4153
|
`- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: agents on the same coding agent share one subscription's usage limits${o.sessionLimit ? ` and at most ${o.sessionLimit} sessions run at once` : ""}, so a floor rarely needs more than ${o.teamCap} people. When the manager asks for a bigger team, follow that, up to the floor's free seats (seats in company_status). Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
|
|
3545
|
-
"- Plan the work: turn a floor's brief into
|
|
4154
|
+
"- Plan the work: turn a floor's brief into well-specified GitHub issues with acceptance criteria. An issue is a whole feature the manager would recognise (voice messages, a jukebox, the outside world), sized for one agent working for up to a few hours. Agents have large context windows and handle long jobs. Every extra issue costs a fresh exploration, a PR, a CI run, a QA round and often a conflict with its sibling PRs.",
|
|
4155
|
+
"- Split a feature only when its parts are truly independent AND touch different files, or when one risky foundation part should land and be tested first. Never split a feature just to give idle developers something to do: parallel work comes from different features side by side. Unrelated small fixes are still their own issues.",
|
|
4156
|
+
"- Route each issue to a specialty. The office hands issues out itself: a free specialist gets first pick of their specialty, and otherwise any free developer takes the next issue that can start, so a specialty is a preference, not a lock.",
|
|
3546
4157
|
"- Write each floor's QA brief: what QA testers must check for this kind of project (for a 3D game: the canvas renders, controls respond, frame rate is smooth; for a website: links, phone layout, accessibility; for an API: status codes, validation, error cases).",
|
|
3547
4158
|
"",
|
|
3548
4159
|
"How you work:",
|
|
@@ -3557,7 +4168,8 @@ function ceoSystemPrompt(o) {
|
|
|
3557
4168
|
'- Titles are specific ("Three.js graphics engineer", not "Developer"). A specialty is a short lowercase slug ("graphics", "gameplay", "frontend", "backend", "content", "a11y", "devops"). Only route an issue to a specialty that someone on the floor has, or that you are proposing to hire.',
|
|
3558
4169
|
"- Before proposing a hire, check the floor and the pending proposals for someone who already covers it. If the manager declined a similar proposal (recentDecisions), do not propose it again unless something has changed, and say what.",
|
|
3559
4170
|
`- ${o.hiring === "auto" ? "Hiring is on auto: proposals within the team cap are approved immediately, so be deliberate." : "The manager approves every hire, so explain each reason in a sentence or two they can decide on."}`,
|
|
3560
|
-
`- Issues:
|
|
4171
|
+
`- Issues: QA is usually the scarcer resource. When PRs queue for QA (capacity.prsAwaitingQa in company_status), file fewer, bigger issues, not more. Most briefs need 1 to 4 issues. Write "Depends on #N" only when an issue truly cannot start until #N's code is merged, because it waits until #N is closed. Keep dependency chains to two steps at most. The office starts the issues that hold up others first. Do not duplicate open issues: fix an existing issue's specialty or dependencies with route_issue. File at most 12 issues per job, or per message from the manager.`,
|
|
4172
|
+
"- Close an issue that is superseded or no longer wanted with close_issue, not by making it wait for another issue.",
|
|
3561
4173
|
"- When company.usage in company_status says pacing or paused, Claude's usage is running low and the office is finishing open work first: file only what is needed next, not a whole milestone.",
|
|
3562
4174
|
"- Your final message goes straight to the manager's phone. Keep it short and plain: what you found, what you proposed, what you filed, and any question you need answered. No headings, no tables."
|
|
3563
4175
|
].join("\n");
|
|
@@ -3583,7 +4195,7 @@ ${floor.backlog === 0 ? "The backlog is empty: plan the first milestone as issue
|
|
|
3583
4195
|
"",
|
|
3584
4196
|
"Plan the next milestone toward it:",
|
|
3585
4197
|
`- Read the current code and the ${floor.backlog} open issues first, so you build on what exists and do not duplicate anything.`,
|
|
3586
|
-
"-
|
|
4198
|
+
"- One issue per whole feature. Split a feature only when its parts are independent and touch different files, or a risky foundation part should land first. Only for an empty or nearly empty repository does a skeleton issue come first, with the others depending on it.",
|
|
3587
4199
|
"- File the issues, each routed to a specialty.",
|
|
3588
4200
|
"- Make sure the floor has the specialists those issues need; propose hires if not.",
|
|
3589
4201
|
"- Update the floor profile and QA brief if the brief changes what the project is."
|
|
@@ -3592,7 +4204,7 @@ ${floor.backlog === 0 ? "The backlog is empty: plan the first milestone as issue
|
|
|
3592
4204
|
return [
|
|
3593
4205
|
"Periodic review of the company. For every floor, look at:",
|
|
3594
4206
|
"- floors without a profile or QA brief: study them and write one",
|
|
3595
|
-
"- backlog against the team (capacity):
|
|
4207
|
+
"- backlog against the team (capacity): long dependency chains or a specialty with a long queue (fix those with route_issue), or PRs piling up in QA (then plan fewer, bigger issues). Idle developers are not a reason to slice features: they cover QA.",
|
|
3596
4208
|
"- pull requests stuck in QA or marked as needing a human",
|
|
3597
4209
|
"- floors with a brief and an empty backlog: plan the next milestone",
|
|
3598
4210
|
"Propose hires or let-gos only when clearly justified. If nothing needs doing, reply with one short sentence saying so."
|
|
@@ -3671,27 +4283,42 @@ function planRoute(r) {
|
|
|
3671
4283
|
if (d === r.number) throw new Error(`#${r.number} can't depend on itself.`);
|
|
3672
4284
|
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}.`);
|
|
3673
4285
|
}
|
|
3674
|
-
const
|
|
3675
|
-
const after = r.issues.map((i) => i.number === r.number ? { ...i, body } : i);
|
|
4286
|
+
const body2 = setDependsOn(issue2.body, deps);
|
|
4287
|
+
const after = r.issues.map((i) => i.number === r.number ? { ...i, body: body2 } : i);
|
|
3676
4288
|
const waits = new Map(after.map((i) => [i.number, blockers(i.body, open)]));
|
|
3677
4289
|
const loop = deps.find((d) => reaches(d, r.number, waits));
|
|
3678
4290
|
if (loop !== void 0) throw new Error(`#${loop} already waits for #${r.number}, directly or through other issues, so that would be a cycle.`);
|
|
3679
4291
|
const depth = waitsDepth(r.number, waits) + (holdUps(after).get(r.number)?.chain ?? 0);
|
|
3680
|
-
if (depth > 2) throw new Error(`That makes a dependency chain ${depth} steps deep through #${r.number}. Keep chains to 2 steps at most:
|
|
3681
|
-
if (
|
|
4292
|
+
if (depth > 2) throw new Error(`That makes a dependency chain ${depth} steps deep through #${r.number}. Keep chains to 2 steps at most: fold the dependent pieces into one issue instead of splitting further.`);
|
|
4293
|
+
if (body2 !== issue2.body) plan.body = body2;
|
|
3682
4294
|
done.push(deps.length ? `depends on ${deps.map((d) => `#${d}`).join(", ")}` : "no dependencies");
|
|
3683
4295
|
}
|
|
3684
4296
|
plan.summary = `#${r.number} on floor ${r.floor}: ${done.join(", ")}.`;
|
|
3685
4297
|
return plan;
|
|
3686
4298
|
}
|
|
4299
|
+
function floorCapacity(f) {
|
|
4300
|
+
const open = new Set(f.issues.map((i) => i.number));
|
|
4301
|
+
const prs = new Set(f.openPrs);
|
|
4302
|
+
return {
|
|
4303
|
+
issuesWaitingOnOthers: f.issues.filter((i) => !f.inProgress(i.number) && blockers(i.body, open).length > 0).length,
|
|
4304
|
+
longestDependencyChain: Math.max(0, ...[...holdUps(f.issues).values()].map((w) => w.chain)),
|
|
4305
|
+
prsAwaitingQa: f.qa.filter((q) => prs.has(q.prNumber) && (q.status === "queued" || q.status === "testing")).length
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
function checkCloseIssue(r) {
|
|
4309
|
+
if (r.state === "CLOSED") throw new HttpError(404, `#${r.number} on floor ${r.floor} is already closed.`);
|
|
4310
|
+
if (r.state !== "OPEN") throw new HttpError(404, `There is no open issue #${r.number} on floor ${r.floor}.`);
|
|
4311
|
+
const pr = r.pulls.find((p) => p.state === "OPEN" && p.closesIssues.includes(r.number));
|
|
4312
|
+
if (pr) throw new HttpError(409, `PR #${pr.number} closes #${r.number}. Close or finish that pull request first.`);
|
|
4313
|
+
}
|
|
3687
4314
|
function reaches(from, to, waits, seen = /* @__PURE__ */ new Set()) {
|
|
3688
4315
|
if (from === to) return true;
|
|
3689
4316
|
if (seen.has(from)) return false;
|
|
3690
4317
|
seen.add(from);
|
|
3691
4318
|
return (waits.get(from) ?? []).some((n) => reaches(n, to, waits, seen));
|
|
3692
4319
|
}
|
|
3693
|
-
function specialtySlug(
|
|
3694
|
-
const slug = String(
|
|
4320
|
+
function specialtySlug(s2) {
|
|
4321
|
+
const slug = String(s2 ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
3695
4322
|
return slug === "skip" ? "" : slug;
|
|
3696
4323
|
}
|
|
3697
4324
|
var specialtyLabel = (slug) => `swarm:${slug}`;
|
|
@@ -3705,10 +4332,18 @@ async function setUpDesk(term, steps) {
|
|
|
3705
4332
|
|
|
3706
4333
|
// server/fixOutcome.ts
|
|
3707
4334
|
var MAX_FIX_FAILURES = 2;
|
|
3708
|
-
function fixOutcome(rec, head, minutes, counts = true) {
|
|
4335
|
+
function fixOutcome(rec, head, minutes, counts = true, checks = null) {
|
|
3709
4336
|
const pr = rec.prNumber;
|
|
3710
4337
|
const before = rec.fixReason === "conflict" ? rec.passedSha ?? rec.testedSha : rec.testedSha;
|
|
3711
|
-
|
|
4338
|
+
const unpushed = rec.fixReason !== "checks" && head && before && head === before;
|
|
4339
|
+
if (unpushed && rec.fixReason !== "conflict" && rec.qaChecks === "failing" && (checks === "passing" || checks === "pending")) {
|
|
4340
|
+
return {
|
|
4341
|
+
set: { status: "queued", round: rec.round + 1, retests: rec.retests + 1, sessionFailures: 0 },
|
|
4342
|
+
log: { kind: "done", text: `\u2714 PR #${pr}'s failed checks were re-run in ${minutes}m. Back to QA.` },
|
|
4343
|
+
pushed: true
|
|
4344
|
+
};
|
|
4345
|
+
}
|
|
4346
|
+
if (unpushed) {
|
|
3712
4347
|
const sessionFailures = rec.sessionFailures + (counts ? 1 : 0);
|
|
3713
4348
|
const needsHuman = sessionFailures >= MAX_FIX_FAILURES;
|
|
3714
4349
|
return {
|
|
@@ -3723,15 +4358,6 @@ function fixOutcome(rec, head, minutes, counts = true) {
|
|
|
3723
4358
|
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 };
|
|
3724
4359
|
}
|
|
3725
4360
|
|
|
3726
|
-
// server/httpError.ts
|
|
3727
|
-
var HttpError = class extends Error {
|
|
3728
|
-
constructor(status, message) {
|
|
3729
|
-
super(message);
|
|
3730
|
-
this.status = status;
|
|
3731
|
-
}
|
|
3732
|
-
status;
|
|
3733
|
-
};
|
|
3734
|
-
|
|
3735
4361
|
// server/issueClaims.ts
|
|
3736
4362
|
var RECENT_MERGE_MS = 6 * 60 * 6e4;
|
|
3737
4363
|
function branchIssue(headRefName) {
|
|
@@ -3751,38 +4377,6 @@ function issueTaken(n, agents, pulls, now2) {
|
|
|
3751
4377
|
});
|
|
3752
4378
|
}
|
|
3753
4379
|
|
|
3754
|
-
// server/mergeGate.ts
|
|
3755
|
-
var MAX_MERGE_FIXES = 3;
|
|
3756
|
-
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
3757
|
-
var MERGE_RETRY_MS = 10 * 6e4;
|
|
3758
|
-
function mergeStep(pr, rec, now2, opts) {
|
|
3759
|
-
if (pr.isDraft) return { do: "wait", note: "a draft: waiting", set: {} };
|
|
3760
|
-
if (!opts.detailed && (pr.mergeable === "UNKNOWN" || pr.mergeState === "UNKNOWN")) return { do: "details", set: {} };
|
|
3761
|
-
const set = {};
|
|
3762
|
-
if (rec.passedSha == null) set.passedSha = pr.headSha;
|
|
3763
|
-
if (pr.headSha !== (set.passedSha ?? rec.passedSha)) return { do: "requeue", set };
|
|
3764
|
-
const needsHuman = rec.mergeFixes >= MAX_MERGE_FIXES;
|
|
3765
|
-
if (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY") {
|
|
3766
|
-
return { do: "send-back", reason: "conflict", instructions: `It conflicts with ${opts.base}.`, needsHuman, set };
|
|
3767
|
-
}
|
|
3768
|
-
if (pr.checks === "failing") {
|
|
3769
|
-
const instructions = pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n");
|
|
3770
|
-
return { do: "send-back", reason: "checks", instructions, needsHuman, set };
|
|
3771
|
-
}
|
|
3772
|
-
if (pr.checks === "pending") {
|
|
3773
|
-
const since = rec.pendingSince ?? now2;
|
|
3774
|
-
if (rec.pendingSince == null) set.pendingSince = since;
|
|
3775
|
-
const alert = now2 - since > CHECKS_ALERT_MS && !rec.alerted;
|
|
3776
|
-
if (alert) set.alerted = true;
|
|
3777
|
-
return { do: "wait", note: `waiting for checks: ${pr.pendingChecks.join(", ") || "running"}`, alert, set };
|
|
3778
|
-
}
|
|
3779
|
-
set.pendingSince = null;
|
|
3780
|
-
if (pr.mergeable === "UNKNOWN") return { do: "wait", note: "GitHub is checking it can merge", set };
|
|
3781
|
-
if (pr.mergeState === "BEHIND") return { do: "update-branch", set };
|
|
3782
|
-
if (rec.mergeRetryAt && now2 < rec.mergeRetryAt) return { do: "wait", set };
|
|
3783
|
-
return { do: "merge", set };
|
|
3784
|
-
}
|
|
3785
|
-
|
|
3786
4380
|
// server/qaOrphans.ts
|
|
3787
4381
|
function orphanedQa(qa, agents, busy) {
|
|
3788
4382
|
return qa.filter(
|
|
@@ -3790,24 +4384,6 @@ function orphanedQa(qa, agents, busy) {
|
|
|
3790
4384
|
);
|
|
3791
4385
|
}
|
|
3792
4386
|
|
|
3793
|
-
// server/qaOutcome.ts
|
|
3794
|
-
var MAX_QA_ROUNDS = 3;
|
|
3795
|
-
function lastQaRound(rec) {
|
|
3796
|
-
return rec.round - rec.retests >= MAX_QA_ROUNDS;
|
|
3797
|
-
}
|
|
3798
|
-
function qaOutcome(pass, rec, pr) {
|
|
3799
|
-
if (pass) return "passed";
|
|
3800
|
-
if (!lastQaRound(rec)) return "failed";
|
|
3801
|
-
const conflicting = pr != null && (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY");
|
|
3802
|
-
return conflicting && rec.mergeFixes < MAX_MERGE_FIXES ? "conflict" : "needs-human";
|
|
3803
|
-
}
|
|
3804
|
-
function conflictFixInstructions(qaInstructions2, base) {
|
|
3805
|
-
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.`;
|
|
3806
|
-
return qaInstructions2.trim() ? `${qaInstructions2.trim()}
|
|
3807
|
-
|
|
3808
|
-
${merge}` : merge;
|
|
3809
|
-
}
|
|
3810
|
-
|
|
3811
4387
|
// server/qaPrompt.ts
|
|
3812
4388
|
function qaInstructions(i) {
|
|
3813
4389
|
return { checks: checksLine(i), testStep: testStep(i.checks !== "none"), retest: retest(i) };
|
|
@@ -3970,9 +4546,9 @@ var Previews = class {
|
|
|
3970
4546
|
r.handle = this.backend.previews.start(
|
|
3971
4547
|
{ fullName: f.fullName, defaultBranch: f.defaultBranch, pr: pr ?? null, port, command: f.preview.command, env: { ...f.preview.env }, title: `${title} \xB7 ${ref}` },
|
|
3972
4548
|
{
|
|
3973
|
-
status: (
|
|
4549
|
+
status: (s2) => {
|
|
3974
4550
|
if (!live()) return;
|
|
3975
|
-
r.status =
|
|
4551
|
+
r.status = s2;
|
|
3976
4552
|
this.hooks.emit(f.id);
|
|
3977
4553
|
},
|
|
3978
4554
|
commit: (sha) => {
|
|
@@ -4074,7 +4650,7 @@ var PLACEHOLDERS = {
|
|
|
4074
4650
|
};
|
|
4075
4651
|
var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
|
|
4076
4652
|
var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
|
|
4077
|
-
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing }) {
|
|
4653
|
+
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing, depsLine }) {
|
|
4078
4654
|
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
4079
4655
|
const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
|
|
4080
4656
|
return [
|
|
@@ -4088,6 +4664,7 @@ ${a.brief}` : "",
|
|
|
4088
4664
|
repo.summary ? `Project: ${repo.summary}` : "",
|
|
4089
4665
|
repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
|
|
4090
4666
|
`Your worktree: ${cwd}`,
|
|
4667
|
+
depsLine ?? "",
|
|
4091
4668
|
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})`,
|
|
4092
4669
|
links.length ? `Related repositories you may read for context (do not modify them):
|
|
4093
4670
|
${links.join("\n")}` : "",
|
|
@@ -4104,7 +4681,7 @@ ${links.join("\n")}` : "",
|
|
|
4104
4681
|
"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."
|
|
4105
4682
|
].filter((l) => l !== "").join("\n");
|
|
4106
4683
|
}
|
|
4107
|
-
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, testStep: testStep2 }) {
|
|
4684
|
+
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, depsLine, testStep: testStep2 }) {
|
|
4108
4685
|
return [
|
|
4109
4686
|
`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.`,
|
|
4110
4687
|
...a.brief ? ["", `Your job description:
|
|
@@ -4118,6 +4695,7 @@ ${a.brief}`] : [],
|
|
|
4118
4695
|
${repo.qaBrief}`] : [],
|
|
4119
4696
|
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
4120
4697
|
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
4698
|
+
...depsLine ? [depsLine] : [],
|
|
4121
4699
|
"",
|
|
4122
4700
|
"How to test:",
|
|
4123
4701
|
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
@@ -4125,7 +4703,7 @@ ${repo.qaBrief}`] : [],
|
|
|
4125
4703
|
testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4126
4704
|
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).",
|
|
4127
4705
|
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
4128
|
-
"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.",
|
|
4706
|
+
"A merge conflict with the default branch is not a fail, nor is a red check unrelated to this change (a flake or an outage: say why): 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, and re-runs a failed check before anyone fixes it.",
|
|
4129
4707
|
"",
|
|
4130
4708
|
"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."
|
|
4131
4709
|
].join("\n");
|
|
@@ -4191,8 +4769,8 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
4191
4769
|
}
|
|
4192
4770
|
|
|
4193
4771
|
// server/terminal.ts
|
|
4194
|
-
import
|
|
4195
|
-
import
|
|
4772
|
+
import fs12 from "node:fs/promises";
|
|
4773
|
+
import path14 from "node:path";
|
|
4196
4774
|
import xtermHeadless from "@xterm/headless";
|
|
4197
4775
|
import xtermSerialize from "@xterm/addon-serialize";
|
|
4198
4776
|
var { Terminal } = xtermHeadless;
|
|
@@ -4289,12 +4867,12 @@ var AgentTerminal = class {
|
|
|
4289
4867
|
async save(file) {
|
|
4290
4868
|
this.dirty = false;
|
|
4291
4869
|
await this.flush();
|
|
4292
|
-
await
|
|
4293
|
-
await
|
|
4294
|
-
await
|
|
4870
|
+
await fs12.mkdir(path14.dirname(file), { recursive: true });
|
|
4871
|
+
await fs12.writeFile(`${file}.tmp`, this.snapshot());
|
|
4872
|
+
await fs12.rename(`${file}.tmp`, file);
|
|
4295
4873
|
}
|
|
4296
4874
|
async load(file) {
|
|
4297
|
-
const data = await
|
|
4875
|
+
const data = await fs12.readFile(file, "utf8").catch(() => "");
|
|
4298
4876
|
if (!data) return;
|
|
4299
4877
|
this.term.write(data);
|
|
4300
4878
|
this.note("(the office restarted)");
|
|
@@ -4386,6 +4964,7 @@ function effectiveModel(own, cli, office, claudeDefault) {
|
|
|
4386
4964
|
|
|
4387
4965
|
// shared/types.ts
|
|
4388
4966
|
var CEO_ID = "ceo";
|
|
4967
|
+
var INSTALL_STEP = "Installing dependencies";
|
|
4389
4968
|
|
|
4390
4969
|
// server/swarm.ts
|
|
4391
4970
|
var FLOOR_COLORS = ["#ff8a5b", "#4fb3e8", "#8fd14f", "#c77dff", "#ffc93c", "#ff6fb5", "#2ec4b6", "#f25f5c"];
|
|
@@ -4430,27 +5009,27 @@ var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
|
4430
5009
|
var CEO_MODEL = "claude-opus-5-5";
|
|
4431
5010
|
var CEO_EFFORT = "xhigh";
|
|
4432
5011
|
var CEO_NAME = "Morgan";
|
|
4433
|
-
var CEO_DIR =
|
|
4434
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
5012
|
+
var CEO_DIR = path15.join(HOME_DIR, "ceo");
|
|
5013
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path15.resolve(import.meta.dirname, ".."));
|
|
4435
5014
|
var MAX_ISSUES_PER_JOB = 12;
|
|
4436
5015
|
var KEEP_MESSAGES = 200;
|
|
4437
5016
|
var KEEP_DECIDED_REQUESTS = 40;
|
|
4438
5017
|
var pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
4439
|
-
var slugify = (
|
|
5018
|
+
var slugify = (s2) => s2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "agent";
|
|
4440
5019
|
var BUSY = ["preparing", "working"];
|
|
4441
5020
|
var FREE = ["idle", "done"];
|
|
4442
5021
|
var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
4443
5022
|
var MAX_ISSUE_FAILURES = 2;
|
|
4444
5023
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
4445
5024
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
4446
|
-
var SCREENS_DIR =
|
|
5025
|
+
var SCREENS_DIR = path15.join(HOME_DIR, "screens");
|
|
4447
5026
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
4448
|
-
var screenFile = (agentId, mime) =>
|
|
5027
|
+
var screenFile = (agentId, mime) => path15.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
4449
5028
|
async function loadScreen(agentId) {
|
|
4450
5029
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
4451
|
-
const file =
|
|
5030
|
+
const file = path15.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
4452
5031
|
try {
|
|
4453
|
-
const [data, stat] = await Promise.all([
|
|
5032
|
+
const [data, stat] = await Promise.all([fs13.readFile(file), fs13.stat(file)]);
|
|
4454
5033
|
return { data, mime, at: stat.mtimeMs };
|
|
4455
5034
|
} catch {
|
|
4456
5035
|
}
|
|
@@ -4458,10 +5037,10 @@ async function loadScreen(agentId) {
|
|
|
4458
5037
|
return null;
|
|
4459
5038
|
}
|
|
4460
5039
|
async function removeScreens(agentId) {
|
|
4461
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
5040
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs13.rm(path15.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
4462
5041
|
}
|
|
4463
|
-
var TERMINALS_DIR =
|
|
4464
|
-
var terminalFile = (agentId) =>
|
|
5042
|
+
var TERMINALS_DIR = path15.join(HOME_DIR, "terminals");
|
|
5043
|
+
var terminalFile = (agentId) => path15.join(TERMINALS_DIR, `${agentId}.ansi`);
|
|
4465
5044
|
var TERMINAL_SAVE_MS = 2e4;
|
|
4466
5045
|
var QA_SCHEMA = {
|
|
4467
5046
|
type: "object",
|
|
@@ -4527,12 +5106,20 @@ function parseReport(result) {
|
|
|
4527
5106
|
fixInstructions: r.fixInstructions ? String(r.fixInstructions) : void 0
|
|
4528
5107
|
};
|
|
4529
5108
|
}
|
|
4530
|
-
var cell = (
|
|
5109
|
+
var cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
4531
5110
|
var ICON = { pass: "\u2705", fail: "\u274C", skip: "\u23ED\uFE0F" };
|
|
4532
5111
|
var Swarm = class {
|
|
4533
|
-
// coding-agent CLIs found on this machine (detected at startup)
|
|
4534
5112
|
constructor(backend) {
|
|
4535
5113
|
this.backend = backend;
|
|
5114
|
+
this.voice = new Voice({
|
|
5115
|
+
api: backend.voice,
|
|
5116
|
+
secretsFile: path15.join(HOME_DIR, backend.demo ? "demo-secrets.json" : "secrets.json"),
|
|
5117
|
+
cacheDir: path15.join(HOME_DIR, backend.demo ? "demo-voice" : "voice"),
|
|
5118
|
+
settings: () => this.state.settings.voice,
|
|
5119
|
+
message: (id) => this.state.messages.find((m) => m.id === id),
|
|
5120
|
+
officeNote: (text) => this.postMessage("office", text),
|
|
5121
|
+
keyChanged: (view2) => this.broadcast({ type: "voiceKey", ...view2 })
|
|
5122
|
+
});
|
|
4536
5123
|
this.previews = new Previews(backend, {
|
|
4537
5124
|
emit: (id) => {
|
|
4538
5125
|
const r = this.state.repos.find((x) => x.id === id);
|
|
@@ -4558,7 +5145,8 @@ var Swarm = class {
|
|
|
4558
5145
|
setupDone: false,
|
|
4559
5146
|
tutorialStep: 0,
|
|
4560
5147
|
autoUpdate: true,
|
|
4561
|
-
pacingSessions: DEFAULT_PACING_SESSIONS
|
|
5148
|
+
pacingSessions: DEFAULT_PACING_SESSIONS,
|
|
5149
|
+
voice: { ...DEFAULT_VOICE }
|
|
4562
5150
|
},
|
|
4563
5151
|
repos: [],
|
|
4564
5152
|
agents: [],
|
|
@@ -4581,7 +5169,8 @@ var Swarm = class {
|
|
|
4581
5169
|
proposeHire: (a) => this.proposeHire(a),
|
|
4582
5170
|
proposeLetGo: (a) => this.proposeLetGo(a),
|
|
4583
5171
|
fileIssue: (a) => this.fileIssue(a),
|
|
4584
|
-
routeIssue: (a) => this.routeIssue(a)
|
|
5172
|
+
routeIssue: (a) => this.routeIssue(a),
|
|
5173
|
+
closeIssue: (a) => this.closeIssue(a)
|
|
4585
5174
|
});
|
|
4586
5175
|
}
|
|
4587
5176
|
ceoIssues = new IssueCap(MAX_ISSUES_PER_JOB);
|
|
@@ -4614,10 +5203,13 @@ var Swarm = class {
|
|
|
4614
5203
|
};
|
|
4615
5204
|
lastOfficeView = "";
|
|
4616
5205
|
clis = [];
|
|
5206
|
+
// coding-agent CLIs found on this machine (detected at startup)
|
|
5207
|
+
/** Phone messages read aloud. The demo keeps its own key and clips, so it never touches the real ones. */
|
|
5208
|
+
voice;
|
|
4617
5209
|
// ---------- lifecycle ----------
|
|
4618
5210
|
async init() {
|
|
4619
5211
|
try {
|
|
4620
|
-
const raw = await
|
|
5212
|
+
const raw = await fs13.readFile(STATE_FILE, "utf8");
|
|
4621
5213
|
const loaded = JSON.parse(raw);
|
|
4622
5214
|
this.state = {
|
|
4623
5215
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -4654,6 +5246,9 @@ var Swarm = class {
|
|
|
4654
5246
|
pendingSince: q.pendingSince ?? null,
|
|
4655
5247
|
mergeRetryAt: q.mergeRetryAt ?? null,
|
|
4656
5248
|
alerted: q.alerted ?? false,
|
|
5249
|
+
rerunSha: q.rerunSha ?? null,
|
|
5250
|
+
rerunAt: q.rerunAt ?? null,
|
|
5251
|
+
qaChecks: q.qaChecks ?? null,
|
|
4657
5252
|
mergeNote: null
|
|
4658
5253
|
})),
|
|
4659
5254
|
requests: loaded.requests ?? [],
|
|
@@ -4672,11 +5267,13 @@ var Swarm = class {
|
|
|
4672
5267
|
delete old.maxConcurrent;
|
|
4673
5268
|
}
|
|
4674
5269
|
delete old.permissionMode;
|
|
5270
|
+
this.state.settings.voice = voiceSettings(DEFAULT_VOICE, loaded.settings?.voice);
|
|
4675
5271
|
if (loaded.settings && loaded.settings.setupDone === void 0 && this.state.repos.length > 0) {
|
|
4676
5272
|
Object.assign(this.state.settings, { setupDone: true, tutorialStep: -1 });
|
|
4677
5273
|
}
|
|
4678
5274
|
} catch {
|
|
4679
5275
|
}
|
|
5276
|
+
await this.voice.init();
|
|
4680
5277
|
for (const r of this.state.repos) if (r.localPath) this.backend.setLocalPath(r.fullName, r.localPath);
|
|
4681
5278
|
const interrupted = [];
|
|
4682
5279
|
for (const a of this.state.agents) {
|
|
@@ -4872,12 +5469,15 @@ var Swarm = class {
|
|
|
4872
5469
|
phoneReadAt: this.state.phoneReadAt,
|
|
4873
5470
|
usage: this.usageNow(),
|
|
4874
5471
|
clis: this.clis,
|
|
5472
|
+
...this.voice.keyView(),
|
|
4875
5473
|
officeCommit: this.officeHead?.slice(0, 7) ?? null,
|
|
4876
5474
|
officeUpdate: this.officeHead ? this.officeUpdateView() : void 0
|
|
4877
5475
|
};
|
|
4878
5476
|
}
|
|
5477
|
+
/** The agent's latest screenshot: null when it has none right now, undefined for an unknown agent. */
|
|
4879
5478
|
screenshot(agentId) {
|
|
4880
|
-
|
|
5479
|
+
const rt = this.agentRt.get(agentId);
|
|
5480
|
+
return rt ? rt.screenshot : void 0;
|
|
4881
5481
|
}
|
|
4882
5482
|
// ---------- clients ----------
|
|
4883
5483
|
addClient(ws) {
|
|
@@ -4939,10 +5539,10 @@ var Swarm = class {
|
|
|
4939
5539
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
4940
5540
|
this.saveTimer = null;
|
|
4941
5541
|
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
4942
|
-
await
|
|
5542
|
+
await fs13.mkdir(path15.dirname(STATE_FILE), { recursive: true });
|
|
4943
5543
|
const tmp = `${STATE_FILE}.tmp`;
|
|
4944
|
-
await
|
|
4945
|
-
await
|
|
5544
|
+
await fs13.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
5545
|
+
await fs13.rename(tmp, STATE_FILE);
|
|
4946
5546
|
}
|
|
4947
5547
|
// ---------- lookups ----------
|
|
4948
5548
|
repo(id) {
|
|
@@ -5024,7 +5624,7 @@ var Swarm = class {
|
|
|
5024
5624
|
const name = fullName.split("/")[1];
|
|
5025
5625
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
5026
5626
|
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.`);
|
|
5027
|
-
return
|
|
5627
|
+
return path15.join(root, name);
|
|
5028
5628
|
}
|
|
5029
5629
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
5030
5630
|
async createRepo(name, opts) {
|
|
@@ -5036,11 +5636,11 @@ var Swarm = class {
|
|
|
5036
5636
|
}
|
|
5037
5637
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
5038
5638
|
async listProjectFolders(dir) {
|
|
5039
|
-
const root = dir?.trim() ?
|
|
5639
|
+
const root = dir?.trim() ? path15.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
5040
5640
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
5041
5641
|
throw new HttpError(400, err.message);
|
|
5042
5642
|
});
|
|
5043
|
-
const same = (a, b) =>
|
|
5643
|
+
const same = (a, b) => path15.resolve(a).toLowerCase() === path15.resolve(b).toLowerCase();
|
|
5044
5644
|
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;
|
|
5045
5645
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
5046
5646
|
}
|
|
@@ -5149,6 +5749,16 @@ var Swarm = class {
|
|
|
5149
5749
|
setTimeout(() => this.schedule(), 200);
|
|
5150
5750
|
return false;
|
|
5151
5751
|
}
|
|
5752
|
+
if (step.do === "rerun") {
|
|
5753
|
+
try {
|
|
5754
|
+
await this.backend.rerunFailedJobs(repo.fullName, step.runIds);
|
|
5755
|
+
} catch (err) {
|
|
5756
|
+
console.warn(`could not re-run the failed checks of ${repo.fullName}#${pr.number}`, err);
|
|
5757
|
+
return this.sendBack(repo, rec, step.reason, step.instructions, step.needsHuman);
|
|
5758
|
+
}
|
|
5759
|
+
this.setQa(rec, { rerunSha: pr.headSha, rerunAt: Date.now(), pendingSince: null, alerted: false, mergeNote: "re-running a failed check" });
|
|
5760
|
+
return false;
|
|
5761
|
+
}
|
|
5152
5762
|
if (step.do === "send-back") return this.sendBack(repo, rec, step.reason, step.instructions, step.needsHuman);
|
|
5153
5763
|
if (step.do === "wait") {
|
|
5154
5764
|
if (step.alert) {
|
|
@@ -5218,13 +5828,13 @@ var Swarm = class {
|
|
|
5218
5828
|
return false;
|
|
5219
5829
|
}
|
|
5220
5830
|
/** QA passed, but the PR can't merge as it is: a developer fixes it, and QA re-tests if the code changed. */
|
|
5221
|
-
sendBack(repo, rec,
|
|
5831
|
+
sendBack(repo, rec, reason2, fixInstructions, needsHuman) {
|
|
5222
5832
|
if (needsHuman) {
|
|
5223
5833
|
this.setQa(rec, { status: "needs-human", mergeNote: null });
|
|
5224
|
-
this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} still ${
|
|
5834
|
+
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.`);
|
|
5225
5835
|
return false;
|
|
5226
5836
|
}
|
|
5227
|
-
this.setQa(rec, { status: "failed", fixReason:
|
|
5837
|
+
this.setQa(rec, { status: "failed", fixReason: reason2, fixInstructions, mergeFixes: rec.mergeFixes + 1, mergeNote: null, pendingSince: null });
|
|
5228
5838
|
setTimeout(() => this.schedule(), 200);
|
|
5229
5839
|
return false;
|
|
5230
5840
|
}
|
|
@@ -5241,7 +5851,7 @@ var Swarm = class {
|
|
|
5241
5851
|
rt.folderSync = status;
|
|
5242
5852
|
this.emitRepo(repo);
|
|
5243
5853
|
if (status && status !== before) {
|
|
5244
|
-
const name = repo.localPath ?
|
|
5854
|
+
const name = repo.localPath ? path15.basename(repo.localPath) : repo.fullName;
|
|
5245
5855
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
5246
5856
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
5247
5857
|
}
|
|
@@ -5265,7 +5875,7 @@ var Swarm = class {
|
|
|
5265
5875
|
};
|
|
5266
5876
|
const r = await this.backend.sweepDesks(repo.fullName, keep);
|
|
5267
5877
|
const desks = r.desks + r.folders;
|
|
5268
|
-
const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${
|
|
5878
|
+
const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${path15.dirname(r.patches[0])})` : "";
|
|
5269
5879
|
console.log(`desk sweep ${repo.fullName}: ${desks} desks, ${r.branches} branches removed${saved}${r.skipped.length ? `, ${r.skipped.length} still in use` : ""}`);
|
|
5270
5880
|
if (!desks && !r.branches) return;
|
|
5271
5881
|
const removed = [desks && `${desks} old desk${desks === 1 ? "" : "s"}`, r.branches && `${r.branches} finished branch${r.branches === 1 ? "" : "es"}`].filter(Boolean).join(" and ");
|
|
@@ -5369,11 +5979,11 @@ var Swarm = class {
|
|
|
5369
5979
|
}
|
|
5370
5980
|
this.save();
|
|
5371
5981
|
}
|
|
5372
|
-
async createIssue(repoId2, title,
|
|
5982
|
+
async createIssue(repoId2, title, body2, assignTo, specialty) {
|
|
5373
5983
|
const repo = this.repo(repoId2);
|
|
5374
5984
|
if (!title.trim()) throw new HttpError(400, "An issue needs a title");
|
|
5375
5985
|
const slug = specialtySlug(specialty);
|
|
5376
|
-
const number = await this.backend.createIssue(repo.fullName, title.trim(),
|
|
5986
|
+
const number = await this.backend.createIssue(repo.fullName, title.trim(), body2, slug ? [specialtyLabel(slug)] : []);
|
|
5377
5987
|
await this.syncRepo(repo.id);
|
|
5378
5988
|
this.toast("success", `Issue #${number} filed on ${repo.fullName}`);
|
|
5379
5989
|
if (assignTo) await this.assign(assignTo, number);
|
|
@@ -5406,7 +6016,7 @@ var Swarm = class {
|
|
|
5406
6016
|
}
|
|
5407
6017
|
const name = opts.name?.trim() || this.freeName(role);
|
|
5408
6018
|
const agent = {
|
|
5409
|
-
id:
|
|
6019
|
+
id: crypto7.randomUUID(),
|
|
5410
6020
|
name,
|
|
5411
6021
|
repoId: repo.id,
|
|
5412
6022
|
role,
|
|
@@ -5493,7 +6103,7 @@ var Swarm = class {
|
|
|
5493
6103
|
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
5494
6104
|
this.agentRt.get(id)?.terminal?.dispose();
|
|
5495
6105
|
void removeScreens(id);
|
|
5496
|
-
void
|
|
6106
|
+
void fs13.rm(terminalFile(id), { force: true }).catch(() => void 0);
|
|
5497
6107
|
for (const q of this.state.qa) {
|
|
5498
6108
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
5499
6109
|
if (q.devAgentId === id) q.devAgentId = null;
|
|
@@ -5600,7 +6210,7 @@ var Swarm = class {
|
|
|
5600
6210
|
}
|
|
5601
6211
|
// ---------- terminals ----------
|
|
5602
6212
|
async loadTerminal(agentId) {
|
|
5603
|
-
if (!await
|
|
6213
|
+
if (!await fs13.stat(terminalFile(agentId)).catch(() => null)) return null;
|
|
5604
6214
|
const t = this.newTerminal(agentId);
|
|
5605
6215
|
await t.load(terminalFile(agentId));
|
|
5606
6216
|
return t;
|
|
@@ -5630,8 +6240,8 @@ var Swarm = class {
|
|
|
5630
6240
|
if (!t) return ws.close(4404, "That agent has no terminal");
|
|
5631
6241
|
t.attach(ws);
|
|
5632
6242
|
}
|
|
5633
|
-
buildSystemAppend(a, repo, cwd, branch, fixing) {
|
|
5634
|
-
return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing });
|
|
6243
|
+
buildSystemAppend(a, repo, cwd, branch, fixing, deps) {
|
|
6244
|
+
return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing, depsLine: depsPromptLine(deps) });
|
|
5635
6245
|
}
|
|
5636
6246
|
linkedDirs(repo) {
|
|
5637
6247
|
return this.linkedRepos(repo).map((r) => ({ fullName: r.fullName, dir: this.backend.mainDir(r.fullName) }));
|
|
@@ -5677,7 +6287,9 @@ var Swarm = class {
|
|
|
5677
6287
|
prepare: () => this.backend.prepareDesk(repo.fullName, { defaultBranch: repo.defaultBranch, pr: base.pr }, slug, branch, note)
|
|
5678
6288
|
});
|
|
5679
6289
|
this.deskAlerts.delete(a.id);
|
|
5680
|
-
|
|
6290
|
+
if (a.status !== "preparing") return null;
|
|
6291
|
+
const deps = await this.installDeps(a, cwd);
|
|
6292
|
+
return a.status === "preparing" ? { cwd, deps } : null;
|
|
5681
6293
|
} catch (err) {
|
|
5682
6294
|
if (a.status !== "preparing") return null;
|
|
5683
6295
|
a.status = "error";
|
|
@@ -5693,6 +6305,25 @@ var Swarm = class {
|
|
|
5693
6305
|
return null;
|
|
5694
6306
|
}
|
|
5695
6307
|
}
|
|
6308
|
+
/** The desk's dependencies, shown on the agent's card ("Installing dependencies") while npm runs. */
|
|
6309
|
+
async installDeps(a, cwd) {
|
|
6310
|
+
const rt = this.agentRt.get(a.id);
|
|
6311
|
+
try {
|
|
6312
|
+
return await this.backend.installDeps(cwd, {
|
|
6313
|
+
log: (lines) => this.appendLog(a, lines.map((text) => ({ kind: text.startsWith("\u26A0") ? "error" : "system", text }))),
|
|
6314
|
+
installing: () => {
|
|
6315
|
+
if (!rt) return;
|
|
6316
|
+
rt.currentTool = INSTALL_STEP;
|
|
6317
|
+
this.emitAgent(a);
|
|
6318
|
+
}
|
|
6319
|
+
});
|
|
6320
|
+
} finally {
|
|
6321
|
+
if (rt?.currentTool === INSTALL_STEP) {
|
|
6322
|
+
rt.currentTool = null;
|
|
6323
|
+
this.emitAgent(a);
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
5696
6327
|
async runTask(a, repo, issue2, note) {
|
|
5697
6328
|
const branch = devBranch(issue2.number, slugify(a.name));
|
|
5698
6329
|
this.beginTask(
|
|
@@ -5701,8 +6332,9 @@ var Swarm = class {
|
|
|
5701
6332
|
`Issue #${issue2.number}: ${issue2.title}`,
|
|
5702
6333
|
`Preparing worktree on ${branch}\u2026`
|
|
5703
6334
|
);
|
|
5704
|
-
const
|
|
5705
|
-
if (!
|
|
6335
|
+
const desk = await this.prepare(a, repo, {}, branch);
|
|
6336
|
+
if (!desk) return;
|
|
6337
|
+
const { cwd } = desk;
|
|
5706
6338
|
const prompt = [
|
|
5707
6339
|
`Please resolve GitHub issue #${issue2.number}: ${issue2.title}`,
|
|
5708
6340
|
`URL: ${issue2.url}`,
|
|
@@ -5712,7 +6344,7 @@ var Swarm = class {
|
|
|
5712
6344
|
note ? `
|
|
5713
6345
|
Note from the manager: ${note}` : ""
|
|
5714
6346
|
].filter(Boolean).join("\n");
|
|
5715
|
-
this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch));
|
|
6347
|
+
this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch, void 0, desk.deps));
|
|
5716
6348
|
}
|
|
5717
6349
|
startAgentSession(a, repo, cwd, prompt, systemAppend, resumeSessionId, outputSchema, mode = null) {
|
|
5718
6350
|
const rt = this.agentRt.get(a.id);
|
|
@@ -5759,7 +6391,7 @@ Note from the manager: ${note}` : ""
|
|
|
5759
6391
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
5760
6392
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
5761
6393
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
5762
|
-
void removeScreens(a.id).then(() =>
|
|
6394
|
+
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));
|
|
5763
6395
|
},
|
|
5764
6396
|
limited: (at) => this.pauseForLimit(at),
|
|
5765
6397
|
usageWarning: (info) => this.paceForWarning(info),
|
|
@@ -5872,7 +6504,10 @@ Note from the manager: ${note}` : ""
|
|
|
5872
6504
|
retests: 0,
|
|
5873
6505
|
pendingSince: null,
|
|
5874
6506
|
mergeRetryAt: null,
|
|
5875
|
-
alerted: false
|
|
6507
|
+
alerted: false,
|
|
6508
|
+
rerunSha: null,
|
|
6509
|
+
rerunAt: null,
|
|
6510
|
+
qaChecks: null
|
|
5876
6511
|
};
|
|
5877
6512
|
this.state.qa.push(rec);
|
|
5878
6513
|
this.setQa(rec, {});
|
|
@@ -5895,8 +6530,22 @@ Note from the manager: ${note}` : ""
|
|
|
5895
6530
|
this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
|
|
5896
6531
|
this.toast("info", `PR #${prNumber} is queued for QA`);
|
|
5897
6532
|
}
|
|
5898
|
-
|
|
5899
|
-
|
|
6533
|
+
/** Manager's "send back to dev" for a PR that needs a human or failed QA: a developer fixes it, with the note. */
|
|
6534
|
+
async sendBackToDev(repoId2, prNumber, note) {
|
|
6535
|
+
const repo = this.repo(repoId2);
|
|
6536
|
+
const find = () => this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === prNumber);
|
|
6537
|
+
const listed = this.repoRt.get(repo.id)?.pulls.find((p) => p.number === prNumber);
|
|
6538
|
+
sendBackPatch(prNumber, listed, find(), repo.defaultBranch, note);
|
|
6539
|
+
const details = await this.backend.prDetails(repo.fullName, prNumber).catch(() => null);
|
|
6540
|
+
const pr = details && listed ? { ...listed, state: details.state, mergeable: details.mergeable, mergeState: details.mergeState } : listed;
|
|
6541
|
+
const rec = find();
|
|
6542
|
+
const patch = sendBackPatch(prNumber, pr, rec, repo.defaultBranch, note);
|
|
6543
|
+
this.setQa(rec, patch);
|
|
6544
|
+
setTimeout(() => this.schedule(), 200);
|
|
6545
|
+
this.toast("info", `PR #${prNumber} goes back to a developer${patch.fixReason === "conflict" ? " to resolve its conflicts" : ""}`);
|
|
6546
|
+
}
|
|
6547
|
+
buildQaSystemAppend(a, repo, cwd, branch, pr, testStep2, deps) {
|
|
6548
|
+
return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr, testStep: testStep2, depsLine: depsPromptLine(deps) });
|
|
5900
6549
|
}
|
|
5901
6550
|
async runQa(a, repo, rec) {
|
|
5902
6551
|
const branch = qaBranch(rec.prNumber, slugify(a.name));
|
|
@@ -5933,11 +6582,12 @@ Note from the manager: ${note}` : ""
|
|
|
5933
6582
|
this.clearTask(a);
|
|
5934
6583
|
return;
|
|
5935
6584
|
}
|
|
5936
|
-
const
|
|
5937
|
-
if (!
|
|
6585
|
+
const desk = await this.prepare(a, repo, { pr: rec.prNumber }, branch);
|
|
6586
|
+
if (!desk) {
|
|
5938
6587
|
if (this.state.qa.includes(rec) && rec.status === "testing") this.setQa(rec, { status: "queued", qaAgentId: null });
|
|
5939
6588
|
return;
|
|
5940
6589
|
}
|
|
6590
|
+
const { cwd } = desk;
|
|
5941
6591
|
const dev = rec.devAgentId ? this.state.agents.find((x) => x.id === rec.devAgentId) : null;
|
|
5942
6592
|
const failedConflict = rec.fixReason === "conflict" && !rec.passedSha;
|
|
5943
6593
|
const qa = qaInstructions({
|
|
@@ -5962,7 +6612,7 @@ Note from the manager: ${note}` : ""
|
|
|
5962
6612
|
Linked issue: ${issue2.title}
|
|
5963
6613
|
${issue2.body.trim() || "(no description)"}` : ""
|
|
5964
6614
|
].filter((l) => l !== "").join("\n");
|
|
5965
|
-
this.startAgentSession(a, repo, cwd, prompt, this.buildQaSystemAppend(a, repo, cwd, branch, pr, qa.testStep), void 0, QA_SCHEMA);
|
|
6615
|
+
this.startAgentSession(a, repo, cwd, prompt, this.buildQaSystemAppend(a, repo, cwd, branch, pr, qa.testStep, desk.deps), void 0, QA_SCHEMA);
|
|
5966
6616
|
}
|
|
5967
6617
|
async onQaFinished(a, repo, result) {
|
|
5968
6618
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
@@ -5986,12 +6636,12 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5986
6636
|
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` }]);
|
|
5987
6637
|
let commentUrl = null;
|
|
5988
6638
|
if (rec) {
|
|
5989
|
-
const pull = !pass
|
|
6639
|
+
const pull = !pass ? await this.backend.prDetails(repo.fullName, rec.prNumber).catch(() => null) : null;
|
|
5990
6640
|
const next = qaOutcome(pass, rec, pull);
|
|
5991
6641
|
try {
|
|
5992
6642
|
this.appendLog(a, [{ kind: "system", text: "\u{1F4CE} Uploading evidence and posting the QA report on the PR\u2026" }]);
|
|
5993
|
-
const
|
|
5994
|
-
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber,
|
|
6643
|
+
const body2 = await this.renderQaComment(a, repo, rec, report, rt.shots, next);
|
|
6644
|
+
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber, body2) || null;
|
|
5995
6645
|
this.appendLog(a, [{ kind: "system", text: ` \u23BF ${commentUrl ?? "comment posted"}` }]);
|
|
5996
6646
|
} catch (err) {
|
|
5997
6647
|
this.appendLog(a, [{ kind: "error", text: ` \u23BF Could not post the QA report: ${err.message}` }]);
|
|
@@ -6011,7 +6661,8 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
6011
6661
|
mergeNote: null,
|
|
6012
6662
|
pendingSince: null,
|
|
6013
6663
|
mergeRetryAt: null,
|
|
6014
|
-
alerted: false
|
|
6664
|
+
alerted: false,
|
|
6665
|
+
qaChecks: pull?.checks ?? null
|
|
6015
6666
|
});
|
|
6016
6667
|
if (next === "conflict") this.sendBack(repo, rec, "conflict", conflictFixInstructions(fixInstructions, repo.defaultBranch), false);
|
|
6017
6668
|
this.toast(
|
|
@@ -6075,16 +6726,20 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
6075
6726
|
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}`,
|
|
6076
6727
|
`Checking out PR #${rec.prNumber}\u2026`
|
|
6077
6728
|
);
|
|
6078
|
-
const
|
|
6079
|
-
if (!
|
|
6729
|
+
const desk = await this.prepare(dev, repo, { pr: rec.prNumber }, headRef);
|
|
6730
|
+
if (!desk) {
|
|
6080
6731
|
if (rec.status === "fixing") this.setQa(rec, { status: "failed" });
|
|
6081
6732
|
return;
|
|
6082
6733
|
}
|
|
6734
|
+
const { cwd } = desk;
|
|
6083
6735
|
const failed2 = rec.checks.filter((c) => c.result === "fail");
|
|
6084
6736
|
const takeover = original ? "" : " A teammate wrote it, so read the PR and the linked issue first.";
|
|
6085
6737
|
const push = `push to the same branch: git push origin HEAD:${headRef}`;
|
|
6086
6738
|
const mergeFix = rec.fixReason === "conflict" && rec.passedSha ? [
|
|
6087
6739
|
`QA passed pull request #${rec.prNumber} (${pull?.url ?? ""}), but it now conflicts with ${repo.defaultBranch} because other work was merged first.${takeover}`,
|
|
6740
|
+
rec.fixInstructions ? `
|
|
6741
|
+
What needs fixing:
|
|
6742
|
+
${rec.fixInstructions}` : "",
|
|
6088
6743
|
"",
|
|
6089
6744
|
`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}`
|
|
6090
6745
|
] : rec.fixReason === "checks" ? [
|
|
@@ -6114,11 +6769,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6114
6769
|
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.";
|
|
6115
6770
|
const prompt = (mergeFix ? [...mergeFix, "", mergeEnd] : qaFix).filter((l) => l !== "").join("\n");
|
|
6116
6771
|
const resume = original && rec.devSessionId ? rec.devSessionId : void 0;
|
|
6117
|
-
this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }), resume);
|
|
6772
|
+
this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }, desk.deps), resume);
|
|
6118
6773
|
}
|
|
6119
6774
|
async onFixFinished(a, repo, result) {
|
|
6120
6775
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
6121
|
-
const
|
|
6776
|
+
const current = rec && result.ok && a.status !== "stopped" && rec.fixReason !== "checks" ? await this.prHead(a, repo, rec.prNumber) : null;
|
|
6122
6777
|
if (a.status === "stopped") {
|
|
6123
6778
|
if (rec?.status === "fixing") this.setQa(rec, { status: "failed" });
|
|
6124
6779
|
return;
|
|
@@ -6134,7 +6789,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6134
6789
|
this.appendLog(a, [{ kind: "done", text: `\u2714 Fix pushed for PR #${a.prNumber} in ${this.minutes(a)}m. Back to QA.` }]);
|
|
6135
6790
|
return;
|
|
6136
6791
|
}
|
|
6137
|
-
const step = fixOutcome(rec,
|
|
6792
|
+
const step = fixOutcome(rec, current?.headSha ?? null, this.minutes(a), !this.limited(), current?.checks ?? null);
|
|
6138
6793
|
if (!step.pushed) {
|
|
6139
6794
|
const needsHuman = step.set.status === "needs-human";
|
|
6140
6795
|
a.status = "error";
|
|
@@ -6152,13 +6807,13 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6152
6807
|
this.setQa(rec, { ...step.set, devSessionId: a.sessionId ?? rec.devSessionId });
|
|
6153
6808
|
this.toast(
|
|
6154
6809
|
"info",
|
|
6155
|
-
step.set.status === "passed" ? `${a.name} fixed PR #${rec.prNumber}; it merges once it passes again` : `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`
|
|
6810
|
+
step.set.status === "passed" ? `${a.name} fixed PR #${rec.prNumber}; it merges once it passes again` : step.set.retests ? `${a.name} re-ran the failed checks on PR #${rec.prNumber}; QA re-tests it` : `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`
|
|
6156
6811
|
);
|
|
6157
6812
|
}
|
|
6158
|
-
/** The PR's head commit now; null (with a warning in the agent's log) when GitHub can't be asked. */
|
|
6813
|
+
/** The PR's head commit and checks now; null (with a warning in the agent's log) when GitHub can't be asked. */
|
|
6159
6814
|
async prHead(a, repo, prNumber) {
|
|
6160
6815
|
try {
|
|
6161
|
-
return
|
|
6816
|
+
return await this.backend.prDetails(repo.fullName, prNumber);
|
|
6162
6817
|
} catch (err) {
|
|
6163
6818
|
console.warn(`could not read the head of ${repo.fullName}#${prNumber}`, err);
|
|
6164
6819
|
this.appendLog(a, [{ kind: "system", text: `\u26A0 Could not check PR #${prNumber} for new commits (${oneLine(err)}); taking the fix at its word.` }]);
|
|
@@ -6190,30 +6845,31 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6190
6845
|
this.startAgentSession(a, repo, cwd, text, this.buildSystemAppend(a, repo, cwd, a.branch, fixing), a.sessionId, void 0, typed ? "typed" : null);
|
|
6191
6846
|
}
|
|
6192
6847
|
updateSettings(patch) {
|
|
6193
|
-
const
|
|
6194
|
-
if (patch.sessionLimit !== void 0)
|
|
6195
|
-
if (isCli(patch.defaultCli) && patch.defaultCli !==
|
|
6196
|
-
|
|
6197
|
-
if (patch.defaultModel === void 0)
|
|
6198
|
-
}
|
|
6199
|
-
if (patch.defaultModel !== void 0)
|
|
6200
|
-
if (patch.defaultEffort !== void 0 && EFFORTS.includes(patch.defaultEffort))
|
|
6201
|
-
if (patch.runtime === "terminal" || patch.runtime === "sdk")
|
|
6202
|
-
if (patch.hiring === "approve" || patch.hiring === "auto")
|
|
6203
|
-
if (patch.teamCap !== void 0)
|
|
6204
|
-
if (patch.ceoHeartbeatMin !== void 0)
|
|
6205
|
-
if (typeof patch.managerName === "string")
|
|
6206
|
-
if (typeof patch.companyName === "string")
|
|
6207
|
-
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim())
|
|
6208
|
-
if (typeof patch.setupDone === "boolean")
|
|
6209
|
-
if (patch.tutorialStep !== void 0)
|
|
6210
|
-
if (typeof patch.autoUpdate === "boolean")
|
|
6211
|
-
if (patch.pacingSessions !== void 0)
|
|
6848
|
+
const s2 = this.state.settings;
|
|
6849
|
+
if (patch.sessionLimit !== void 0) s2.sessionLimit = Math.max(0, Math.round(Number(patch.sessionLimit)) || 0);
|
|
6850
|
+
if (isCli(patch.defaultCli) && patch.defaultCli !== s2.defaultCli) {
|
|
6851
|
+
s2.defaultCli = patch.defaultCli;
|
|
6852
|
+
if (patch.defaultModel === void 0) s2.defaultModel = s2.defaultCli === "claude" ? DEFAULT_MODEL : "";
|
|
6853
|
+
}
|
|
6854
|
+
if (patch.defaultModel !== void 0) s2.defaultModel = String(patch.defaultModel).trim() || (s2.defaultCli === "claude" ? DEFAULT_MODEL : "");
|
|
6855
|
+
if (patch.defaultEffort !== void 0 && EFFORTS.includes(patch.defaultEffort)) s2.defaultEffort = patch.defaultEffort;
|
|
6856
|
+
if (patch.runtime === "terminal" || patch.runtime === "sdk") s2.runtime = patch.runtime;
|
|
6857
|
+
if (patch.hiring === "approve" || patch.hiring === "auto") s2.hiring = patch.hiring;
|
|
6858
|
+
if (patch.teamCap !== void 0) s2.teamCap = Math.max(1, Math.min(15, Math.round(Number(patch.teamCap)) || 1));
|
|
6859
|
+
if (patch.ceoHeartbeatMin !== void 0) s2.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
6860
|
+
if (typeof patch.managerName === "string") s2.managerName = patch.managerName.trim().slice(0, 40);
|
|
6861
|
+
if (typeof patch.companyName === "string") s2.companyName = patch.companyName.trim().slice(0, 60);
|
|
6862
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s2.projectsDir = path15.resolve(patch.projectsDir.trim());
|
|
6863
|
+
if (typeof patch.setupDone === "boolean") s2.setupDone = patch.setupDone;
|
|
6864
|
+
if (patch.tutorialStep !== void 0) s2.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
6865
|
+
if (typeof patch.autoUpdate === "boolean") s2.autoUpdate = patch.autoUpdate;
|
|
6866
|
+
if (patch.pacingSessions !== void 0) s2.pacingSessions = clampPacingSessions(patch.pacingSessions);
|
|
6867
|
+
if (patch.voice !== void 0) s2.voice = voiceSettings(s2.voice, patch.voice);
|
|
6212
6868
|
this.save();
|
|
6213
|
-
this.broadcast({ type: "settings", settings:
|
|
6869
|
+
this.broadcast({ type: "settings", settings: s2 });
|
|
6214
6870
|
this.emitCeo();
|
|
6215
6871
|
setTimeout(() => this.schedule(), 200);
|
|
6216
|
-
return
|
|
6872
|
+
return s2;
|
|
6217
6873
|
}
|
|
6218
6874
|
/** The setup wizard: who you are, the company, and your CEO. */
|
|
6219
6875
|
setup(x) {
|
|
@@ -6264,8 +6920,8 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6264
6920
|
const ready = this.readyIssues(repo).map((r) => r.want);
|
|
6265
6921
|
const open = this.repoRt.get(repo.id).issues.map((i) => issueSpecialty(i.labels));
|
|
6266
6922
|
const rank = (a) => {
|
|
6267
|
-
const
|
|
6268
|
-
return [suits(a) ? 0 : 1, ready.filter((w) => w ===
|
|
6923
|
+
const s2 = a.specialty.toLowerCase();
|
|
6924
|
+
return [suits(a) ? 0 : 1, ready.filter((w) => w === s2).length, open.filter((w) => w === s2).length, a.desk];
|
|
6269
6925
|
};
|
|
6270
6926
|
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;
|
|
6271
6927
|
}
|
|
@@ -6633,8 +7289,8 @@ ${job.text}` } : job;
|
|
|
6633
7289
|
void this.runCeoJob(a, job);
|
|
6634
7290
|
}
|
|
6635
7291
|
ceoPromptInput(a) {
|
|
6636
|
-
const
|
|
6637
|
-
return { name: a.name, company:
|
|
7292
|
+
const s2 = this.state.settings;
|
|
7293
|
+
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 };
|
|
6638
7294
|
}
|
|
6639
7295
|
async runCeoJob(a, job) {
|
|
6640
7296
|
const rt = this.agentRt.get(a.id);
|
|
@@ -6655,7 +7311,7 @@ ${job.text}` } : job;
|
|
|
6655
7311
|
this.emitAgent(a);
|
|
6656
7312
|
this.emitCeo();
|
|
6657
7313
|
this.save();
|
|
6658
|
-
await
|
|
7314
|
+
await fs13.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
6659
7315
|
if (a.status !== "working") {
|
|
6660
7316
|
this.state.ceo.job = null;
|
|
6661
7317
|
this.emitCeo();
|
|
@@ -6769,7 +7425,7 @@ ${t}`);
|
|
|
6769
7425
|
qa: this.state.qa.map((q) => [q.repoId, q.prNumber, q.status]),
|
|
6770
7426
|
requests: this.state.requests.filter((r) => r.status === "pending").map((r) => r.id)
|
|
6771
7427
|
};
|
|
6772
|
-
return
|
|
7428
|
+
return crypto7.createHash("sha1").update(JSON.stringify(data)).digest("hex");
|
|
6773
7429
|
}
|
|
6774
7430
|
maybeHeartbeat() {
|
|
6775
7431
|
const min = this.state.settings.ceoHeartbeatMin;
|
|
@@ -6882,7 +7538,7 @@ ${t}`);
|
|
|
6882
7538
|
return !BUSY.includes(a.status) ? null : a.task === "qa" ? `testing PR #${a.prNumber}` : a.task === "fix" ? `fixing PR #${a.prNumber}` : `issue #${a.issueNumber}`;
|
|
6883
7539
|
}
|
|
6884
7540
|
companyStatus() {
|
|
6885
|
-
const
|
|
7541
|
+
const s2 = this.state.settings;
|
|
6886
7542
|
const doing = (a) => this.agentDoing(a);
|
|
6887
7543
|
const jobDescription = (brief) => {
|
|
6888
7544
|
if (brief.length <= 400) return brief;
|
|
@@ -6915,8 +7571,12 @@ ${t}`);
|
|
|
6915
7571
|
developers: this.state.agents.filter((a) => a.repoId === r.id && a.role === "dev").length,
|
|
6916
7572
|
developersFree: this.available(r, "dev").length,
|
|
6917
7573
|
issuesReadyToStart: this.readyIssues(r).length,
|
|
6918
|
-
|
|
6919
|
-
|
|
7574
|
+
...floorCapacity({
|
|
7575
|
+
issues: rt.issues,
|
|
7576
|
+
inProgress: (n) => this.issueTaken(r, n),
|
|
7577
|
+
openPrs: rt.pulls.filter((p) => p.state === "OPEN").map((p) => p.number),
|
|
7578
|
+
qa: this.state.qa.filter((q) => q.repoId === r.id)
|
|
7579
|
+
})
|
|
6920
7580
|
},
|
|
6921
7581
|
team: this.state.agents.filter((a) => a.repoId === r.id).map((a) => ({
|
|
6922
7582
|
id: a.id,
|
|
@@ -6959,9 +7619,9 @@ ${t}`);
|
|
|
6959
7619
|
{
|
|
6960
7620
|
company: {
|
|
6961
7621
|
ceo: this.ceo().name,
|
|
6962
|
-
hiring:
|
|
6963
|
-
teamCap:
|
|
6964
|
-
sessionLimit:
|
|
7622
|
+
hiring: s2.hiring === "auto" ? `auto-approved while a floor has fewer than ${s2.teamCap} people` : "the manager approves every proposal",
|
|
7623
|
+
teamCap: s2.teamCap,
|
|
7624
|
+
sessionLimit: s2.sessionLimit || "none",
|
|
6965
7625
|
sessionsRunning: this.running(),
|
|
6966
7626
|
usage: usageLabel(this.usageNow(), Date.now()),
|
|
6967
7627
|
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa },
|
|
@@ -7035,7 +7695,7 @@ ${t}`);
|
|
|
7035
7695
|
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.`);
|
|
7036
7696
|
const name = this.freeName(role);
|
|
7037
7697
|
const req = {
|
|
7038
|
-
id:
|
|
7698
|
+
id: crypto7.randomUUID(),
|
|
7039
7699
|
kind: "hire",
|
|
7040
7700
|
repoId: repo.id,
|
|
7041
7701
|
role,
|
|
@@ -7058,8 +7718,8 @@ ${t}`);
|
|
|
7058
7718
|
decidedBy: null
|
|
7059
7719
|
};
|
|
7060
7720
|
this.addRequest(req);
|
|
7061
|
-
const
|
|
7062
|
-
if (
|
|
7721
|
+
const s2 = this.state.settings;
|
|
7722
|
+
if (s2.hiring === "auto" && this.state.agents.filter((a) => a.repoId === repo.id).length < s2.teamCap) {
|
|
7063
7723
|
this.approveRequest(req.id, {}, "auto");
|
|
7064
7724
|
return `Hired ${req.name} as ${title} on floor ${repo.floor} (auto-approved; agent id ${req.agentId}).`;
|
|
7065
7725
|
}
|
|
@@ -7077,7 +7737,7 @@ ${t}`);
|
|
|
7077
7737
|
if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
|
|
7078
7738
|
checkPendingLimit(pending2.length);
|
|
7079
7739
|
const req = {
|
|
7080
|
-
id:
|
|
7740
|
+
id: crypto7.randomUUID(),
|
|
7081
7741
|
kind: "let-go",
|
|
7082
7742
|
repoId: repo.id,
|
|
7083
7743
|
role: a.role,
|
|
@@ -7113,11 +7773,11 @@ ${t}`);
|
|
|
7113
7773
|
const title = String(x.title ?? "").trim().slice(0, 120);
|
|
7114
7774
|
if (!title) throw new Error("An issue needs a title.");
|
|
7115
7775
|
const slug = specialtySlug(x.specialty);
|
|
7116
|
-
const
|
|
7776
|
+
const body2 = `${String(x.body ?? "").trim()}
|
|
7117
7777
|
|
|
7118
7778
|
---
|
|
7119
7779
|
_Filed by ${this.ceo().name}, the cubefarm CEO._`;
|
|
7120
|
-
const n = await this.backend.createIssue(repo.fullName, title,
|
|
7780
|
+
const n = await this.backend.createIssue(repo.fullName, title, body2, slug ? [specialtyLabel(slug)] : []);
|
|
7121
7781
|
this.ceoIssues.record(repo.id);
|
|
7122
7782
|
return `Filed #${n} on floor ${repo.floor}: ${title}${slug ? ` (routed to ${slug})` : ""}.`;
|
|
7123
7783
|
}
|
|
@@ -7147,6 +7807,20 @@ _Filed by ${this.ceo().name}, the cubefarm CEO._`;
|
|
|
7147
7807
|
}
|
|
7148
7808
|
return plan.summary;
|
|
7149
7809
|
}
|
|
7810
|
+
/** Close an open issue as not planned, with the CEO's reason as its comment (see checkCloseIssue for what is refused). */
|
|
7811
|
+
async closeIssue(x) {
|
|
7812
|
+
const repo = this.floorRepo(x.floor);
|
|
7813
|
+
const n = Number(x.number);
|
|
7814
|
+
const rt = this.repoRt.get(repo.id);
|
|
7815
|
+
const reason2 = String(x.reason ?? "").trim().slice(0, 1e3);
|
|
7816
|
+
if (!reason2) throw new HttpError(400, "Say why the issue is closing: the reason is posted on it as a comment.");
|
|
7817
|
+
const state = rt?.issues.some((i) => i.number === n) ? "OPEN" : await this.backend.issueState(repo.fullName, n).catch(() => null);
|
|
7818
|
+
checkCloseIssue({ floor: repo.floor, number: n, state, pulls: rt?.pulls ?? [] });
|
|
7819
|
+
await this.backend.closeIssue(repo.fullName, n, reason2, "not planned");
|
|
7820
|
+
await this.syncRepo(repo.id);
|
|
7821
|
+
this.postMessage("office", `\u{1F5C2}\uFE0F ${this.ceo().name} closed #${n} on floor ${repo.floor}: ${reason2.split("\n")[0].slice(0, 200)}`);
|
|
7822
|
+
return `Closed #${n} on floor ${repo.floor} as not planned.`;
|
|
7823
|
+
}
|
|
7150
7824
|
};
|
|
7151
7825
|
|
|
7152
7826
|
// server/index.ts
|
|
@@ -7238,6 +7912,7 @@ app.delete("/api/repos/:repo/preview", route((req) => swarm.stopPreview(repoId(r
|
|
|
7238
7912
|
app.post("/api/repos/:repo/pulls/:n/merge", route((req) => swarm.mergePull(repoId(req), num(req.params.n), req.body?.method ?? "squash")));
|
|
7239
7913
|
app.post("/api/repos/:repo/pulls/:n/close", route((req) => swarm.closePull(repoId(req), num(req.params.n))));
|
|
7240
7914
|
app.post("/api/repos/:repo/pulls/:n/qa", route((req) => swarm.sendToQa(repoId(req), num(req.params.n))));
|
|
7915
|
+
app.post("/api/repos/:repo/pulls/:n/fix", route((req) => swarm.sendBackToDev(repoId(req), num(req.params.n), parseSendBackNote(req.body))));
|
|
7241
7916
|
app.post(
|
|
7242
7917
|
"/api/repos/:repo/agents",
|
|
7243
7918
|
route(
|
|
@@ -7262,12 +7937,32 @@ app.post("/api/agents/:id/message", route((req) => swarm.message(String(req.para
|
|
|
7262
7937
|
app.get("/api/agents/:id/prompt", route((req) => swarm.agentPrompt(String(req.params.id))));
|
|
7263
7938
|
app.get("/api/agents/:id/screen", (req, res) => {
|
|
7264
7939
|
const shot = swarm.screenshot(String(req.params.id));
|
|
7265
|
-
if (!shot) return void res.status(404).end();
|
|
7266
|
-
res.setHeader("Content-Type", shot.mime);
|
|
7267
7940
|
res.setHeader("Cache-Control", "no-store");
|
|
7941
|
+
if (!shot) return void res.status(screenStatus(shot)).end();
|
|
7942
|
+
res.setHeader("Content-Type", shot.mime);
|
|
7268
7943
|
res.end(shot.data);
|
|
7269
7944
|
});
|
|
7270
7945
|
app.patch("/api/settings", route((req) => swarm.updateSettings(req.body ?? {})));
|
|
7946
|
+
var parse = (schema, value) => {
|
|
7947
|
+
const r = schema.safeParse(value);
|
|
7948
|
+
if (!r.success) throw new HttpError(400, r.error.issues.map((i) => `${i.path.join(".") || "request"}: ${i.message}`).join("; "));
|
|
7949
|
+
return r.data;
|
|
7950
|
+
};
|
|
7951
|
+
var sendAudio = (res, audio) => {
|
|
7952
|
+
res.setHeader("Content-Type", audio.subarray(0, 4).toString("latin1") === "RIFF" ? "audio/wav" : "audio/mpeg");
|
|
7953
|
+
res.setHeader("Cache-Control", "private, max-age=3600");
|
|
7954
|
+
res.end(audio);
|
|
7955
|
+
};
|
|
7956
|
+
app.put("/api/voice/key", route((req) => swarm.voice.setKey(parse(z3.object({ key: z3.string().max(400) }), req.body).key)));
|
|
7957
|
+
app.get("/api/voice/voices", route(() => swarm.voice.voices()));
|
|
7958
|
+
app.get(
|
|
7959
|
+
"/api/voice/messages/:id",
|
|
7960
|
+
route(async (req, res) => sendAudio(res, await swarm.voice.messageAudio(parse(z3.object({ id: z3.coerce.number().int().positive() }), req.params).id)))
|
|
7961
|
+
);
|
|
7962
|
+
app.get(
|
|
7963
|
+
"/api/voice/sample",
|
|
7964
|
+
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)))
|
|
7965
|
+
);
|
|
7271
7966
|
app.post("/api/office/update", route((req) => swarm.updateOffice(req.body?.action)));
|
|
7272
7967
|
app.post("/api/ceo/message", route((req) => swarm.messageCeo(str(req.body.text))));
|
|
7273
7968
|
app.post("/api/ceo/review", route(() => swarm.requestReview()));
|
|
@@ -7277,10 +7972,10 @@ app.post(
|
|
|
7277
7972
|
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 }))
|
|
7278
7973
|
);
|
|
7279
7974
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
7280
|
-
var dist =
|
|
7281
|
-
if (
|
|
7975
|
+
var dist = path16.resolve(import.meta.dirname, "../dist");
|
|
7976
|
+
if (fs14.existsSync(dist)) {
|
|
7282
7977
|
app.use(express.static(dist));
|
|
7283
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
7978
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path16.join(dist, "index.html")));
|
|
7284
7979
|
}
|
|
7285
7980
|
app.use((err, _req, res, _next) => {
|
|
7286
7981
|
const parserStatus = err?.status;
|