cubefarm 0.3.9 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/assets/{ToyWorld-CCa77hRS.js → ToyWorld-CgbZTjI5.js} +2 -2
- package/dist/assets/{index-g6QWWXij.js → index-DxPWlxj7.js} +30 -30
- package/dist/assets/index-qBB5_EtY.css +1 -0
- package/dist/assets/{rapier--PAOxOZs.js → rapier-BkzkQl77.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +912 -318
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/assets/index-CIr98Zaa.css +0 -1
package/dist-server/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// server/index.ts
|
|
2
2
|
import http2 from "node:http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path16 from "node:path";
|
|
4
|
+
import fs14 from "node:fs";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
|
+
import { z as z3 } from "zod";
|
|
7
8
|
|
|
8
9
|
// server/config.ts
|
|
9
10
|
import fs from "node:fs";
|
|
@@ -146,11 +147,11 @@ async function ensureLabel(fullName, name) {
|
|
|
146
147
|
await gh(["label", "create", name, "-R", fullName, "--color", "c77dff", "--description", "cubefarm: routed to this specialty", "--force"]);
|
|
147
148
|
labelsMade.add(key);
|
|
148
149
|
}
|
|
149
|
-
async function createIssue(fullName, title,
|
|
150
|
+
async function createIssue(fullName, title, body2, labels = []) {
|
|
150
151
|
for (const l of labels) await ensureLabel(fullName, l);
|
|
151
152
|
const args = ["issue", "create", "-R", fullName, "--title", title, "--body-file", "-"];
|
|
152
153
|
for (const l of labels) args.push("--label", l);
|
|
153
|
-
const out = await gh(args, { input:
|
|
154
|
+
const out = await gh(args, { input: body2 || " " });
|
|
154
155
|
const match = out.match(/\/issues\/(\d+)/);
|
|
155
156
|
if (!match) throw new Error(`Could not read the new issue number from gh output: ${out}`);
|
|
156
157
|
return Number(match[1]);
|
|
@@ -220,13 +221,13 @@ async function issueDetails(fullName, number) {
|
|
|
220
221
|
const raw = await ghJson(["issue", "view", String(number), "-R", fullName, "--json", "title,body"]);
|
|
221
222
|
return { title: raw.title, body: raw.body ?? "" };
|
|
222
223
|
}
|
|
223
|
-
async function commentPull(fullName, number,
|
|
224
|
-
const out = await gh(["pr", "comment", String(number), "-R", fullName, "--body-file", "-"], { input:
|
|
224
|
+
async function commentPull(fullName, number, body2) {
|
|
225
|
+
const out = await gh(["pr", "comment", String(number), "-R", fullName, "--body-file", "-"], { input: body2 });
|
|
225
226
|
return out.match(/https:\/\/github\.com\/\S+/)?.[0] ?? "";
|
|
226
227
|
}
|
|
227
228
|
var EVIDENCE_BRANCH = "swarm-qa-evidence";
|
|
228
229
|
var evidenceReady = /* @__PURE__ */ new Set();
|
|
229
|
-
var ghApiJson = (method, endpoint,
|
|
230
|
+
var ghApiJson = (method, endpoint, body2) => gh(["api", "-X", method, endpoint, "-H", "Content-Type: application/json", "--input", "-"], { input: JSON.stringify(body2), timeoutMs: 12e4 }).then(
|
|
230
231
|
(out) => out ? JSON.parse(out) : null
|
|
231
232
|
);
|
|
232
233
|
async function ensureEvidenceBranch(fullName) {
|
|
@@ -819,14 +820,14 @@ function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
|
819
820
|
}
|
|
820
821
|
return found2;
|
|
821
822
|
}
|
|
822
|
-
var clip = (
|
|
823
|
+
var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
|
|
823
824
|
function rel(cwd, p) {
|
|
824
|
-
const
|
|
825
|
-
if (!
|
|
826
|
-
const r = path4.relative(cwd, path4.resolve(cwd,
|
|
827
|
-
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") :
|
|
825
|
+
const s2 = String(p ?? "");
|
|
826
|
+
if (!s2) return "";
|
|
827
|
+
const r = path4.relative(cwd, path4.resolve(cwd, s2));
|
|
828
|
+
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s2;
|
|
828
829
|
}
|
|
829
|
-
var escapeRe = (
|
|
830
|
+
var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
830
831
|
function tidyPaths(cwd, text) {
|
|
831
832
|
let out = text;
|
|
832
833
|
for (const v of /* @__PURE__ */ new Set([cwd, cwd.replaceAll("\\", "/")])) {
|
|
@@ -1006,7 +1007,7 @@ function startSession(opts, callbacks) {
|
|
|
1006
1007
|
case "system": {
|
|
1007
1008
|
if (msg.subtype === "init") {
|
|
1008
1009
|
cb.sessionId(msg.session_id);
|
|
1009
|
-
const mcp = msg.mcp_servers.map((
|
|
1010
|
+
const mcp = msg.mcp_servers.map((s2) => `${s2.name}:${s2.status}`).join(", ");
|
|
1010
1011
|
cb.log([
|
|
1011
1012
|
{ kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort` },
|
|
1012
1013
|
{ kind: "system", text: ` cwd ${msg.cwd}${mcp ? ` \xB7 mcp ${mcp}` : ""}` }
|
|
@@ -1223,7 +1224,7 @@ async function detectClis() {
|
|
|
1223
1224
|
var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
|
|
1224
1225
|
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
|
|
1225
1226
|
function codexHookCommand(node, script, win = WIN) {
|
|
1226
|
-
return win ? `node "${script}"` : [node, script].map((
|
|
1227
|
+
return win ? `node "${script}"` : [node, script].map((s2) => `'${s2.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
1227
1228
|
}
|
|
1228
1229
|
function launchArgs(id, ctx) {
|
|
1229
1230
|
switch (id) {
|
|
@@ -1253,7 +1254,7 @@ function launchArgs(id, ctx) {
|
|
|
1253
1254
|
return { args, env: { DISABLE_AUTOUPDATER: "1" } };
|
|
1254
1255
|
}
|
|
1255
1256
|
case "codex": {
|
|
1256
|
-
const toml = (
|
|
1257
|
+
const toml = (s2) => JSON.stringify(s2);
|
|
1257
1258
|
const args = [
|
|
1258
1259
|
"--no-alt-screen",
|
|
1259
1260
|
"-c",
|
|
@@ -1657,11 +1658,11 @@ function setOfficeUrl(url) {
|
|
|
1657
1658
|
officeUrl = url;
|
|
1658
1659
|
}
|
|
1659
1660
|
var routes = /* @__PURE__ */ new Map();
|
|
1660
|
-
function handleHook(token,
|
|
1661
|
+
function handleHook(token, body2) {
|
|
1661
1662
|
const route2 = routes.get(token);
|
|
1662
|
-
if (!route2 || !
|
|
1663
|
+
if (!route2 || !body2 || typeof body2 !== "object" || Array.isArray(body2)) return {};
|
|
1663
1664
|
try {
|
|
1664
|
-
return route2.hook(
|
|
1665
|
+
return route2.hook(body2);
|
|
1665
1666
|
} catch (err) {
|
|
1666
1667
|
console.warn("hook failed", err);
|
|
1667
1668
|
return {};
|
|
@@ -1708,7 +1709,7 @@ function toolOutput(response) {
|
|
|
1708
1709
|
if (typeof response === "string") return { text: response, images };
|
|
1709
1710
|
const r = response;
|
|
1710
1711
|
if (r && typeof r === "object") {
|
|
1711
|
-
if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((
|
|
1712
|
+
if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((s2) => typeof s2 === "string" && s2).join("\n"), images };
|
|
1712
1713
|
if (typeof r.file?.content === "string") return { text: r.file.content, images };
|
|
1713
1714
|
if (Array.isArray(r.filenames)) return { text: r.filenames.join("\n"), images };
|
|
1714
1715
|
}
|
|
@@ -2191,19 +2192,137 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
2191
2192
|
return handle;
|
|
2192
2193
|
}
|
|
2193
2194
|
|
|
2194
|
-
// server/
|
|
2195
|
+
// server/deps.ts
|
|
2195
2196
|
import { spawn as spawn2 } from "node:child_process";
|
|
2196
2197
|
import crypto4 from "node:crypto";
|
|
2197
2198
|
import fs8 from "node:fs/promises";
|
|
2198
|
-
import net2 from "node:net";
|
|
2199
2199
|
import path9 from "node:path";
|
|
2200
|
+
var INSTALL_MARKER = ".cubefarm-preview-install";
|
|
2201
|
+
var INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
2202
|
+
var MAX_DESK_INSTALLS = 2;
|
|
2203
|
+
var depsHash = (pkg, lock) => crypto4.createHash("sha256").update(pkg).update("\0").update(lock ?? "").digest("hex");
|
|
2204
|
+
async function readDeps(dir) {
|
|
2205
|
+
const read = (file) => fs8.readFile(path9.join(dir, file), "utf8").catch(() => null);
|
|
2206
|
+
const [pkg, lock, marker, nodeModules] = await Promise.all([
|
|
2207
|
+
read("package.json"),
|
|
2208
|
+
read("package-lock.json"),
|
|
2209
|
+
read(path9.join("node_modules", INSTALL_MARKER)),
|
|
2210
|
+
fs8.stat(path9.join(dir, "node_modules")).then((s2) => s2.isDirectory()).catch(() => false)
|
|
2211
|
+
]);
|
|
2212
|
+
return { pkg, lock, hash: depsHash(pkg ?? "", lock), marker: marker ?? "", nodeModules };
|
|
2213
|
+
}
|
|
2214
|
+
var depsCurrent = (s2) => s2.nodeModules && s2.marker === s2.hash;
|
|
2215
|
+
var writeMarker = (dir, hash) => fs8.writeFile(path9.join(dir, "node_modules", INSTALL_MARKER), hash).catch(() => void 0);
|
|
2216
|
+
function deskDepsPlan(s2) {
|
|
2217
|
+
if (s2.pkg === null) return "none";
|
|
2218
|
+
if (s2.lock === null) return "agent";
|
|
2219
|
+
return depsCurrent(s2) ? "skip" : "install";
|
|
2220
|
+
}
|
|
2221
|
+
function depsPromptLine(outcome) {
|
|
2222
|
+
if (outcome === "installed" || outcome === "skipped") return "Dependencies are already installed for this checkout; run npm install only if you change package.json.";
|
|
2223
|
+
if (outcome === "failed") return "The office couldn't install this checkout's dependencies: run npm install yourself first.";
|
|
2224
|
+
return "";
|
|
2225
|
+
}
|
|
2226
|
+
function createLimiter(max) {
|
|
2227
|
+
let active = 0;
|
|
2228
|
+
const waiting = [];
|
|
2229
|
+
return async function limit(fn) {
|
|
2230
|
+
if (active >= max) await new Promise((r) => waiting.push(r));
|
|
2231
|
+
else active++;
|
|
2232
|
+
try {
|
|
2233
|
+
return await fn();
|
|
2234
|
+
} finally {
|
|
2235
|
+
const next = waiting.shift();
|
|
2236
|
+
if (next) next();
|
|
2237
|
+
else active--;
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
var deskInstalls = createLimiter(MAX_DESK_INSTALLS);
|
|
2242
|
+
function killTree2(child) {
|
|
2243
|
+
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
2244
|
+
if (process.platform === "win32") {
|
|
2245
|
+
return new Promise((resolve) => {
|
|
2246
|
+
const tk = spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
|
|
2247
|
+
tk.once("error", () => resolve());
|
|
2248
|
+
tk.once("close", () => resolve());
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
try {
|
|
2252
|
+
process.kill(-child.pid, "SIGKILL");
|
|
2253
|
+
} catch {
|
|
2254
|
+
child.kill("SIGKILL");
|
|
2255
|
+
}
|
|
2256
|
+
return Promise.resolve();
|
|
2257
|
+
}
|
|
2258
|
+
function npmCi(dir, timeoutMs = INSTALL_TIMEOUT_MS) {
|
|
2259
|
+
const env = {};
|
|
2260
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
2261
|
+
if (v !== void 0 && !/^(ANTHROPIC_|CLAUDE|SWARM_)/i.test(k)) env[k] = v;
|
|
2262
|
+
}
|
|
2263
|
+
env.NO_COLOR = "1";
|
|
2264
|
+
const win = process.platform === "win32";
|
|
2265
|
+
const child = win ? spawn2("cmd.exe", ["/d", "/s", "/c", "npm ci --no-audit --no-fund"], { cwd: dir, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }) : spawn2("npm", ["ci", "--no-audit", "--no-fund"], { cwd: dir, env, windowsHide: true, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
2266
|
+
let tail = "";
|
|
2267
|
+
const keep = (buf) => tail = (tail + buf.toString()).slice(-4e3);
|
|
2268
|
+
child.stdout?.on("data", keep);
|
|
2269
|
+
child.stderr?.on("data", keep);
|
|
2270
|
+
return new Promise((resolve, reject) => {
|
|
2271
|
+
const timer = setTimeout(() => {
|
|
2272
|
+
void killTree2(child);
|
|
2273
|
+
reject(new Error(`npm ci took longer than ${Math.round(timeoutMs / 6e4)} minutes`));
|
|
2274
|
+
}, timeoutMs);
|
|
2275
|
+
child.once("error", (err) => {
|
|
2276
|
+
clearTimeout(timer);
|
|
2277
|
+
reject(err);
|
|
2278
|
+
});
|
|
2279
|
+
child.once("close", (code) => {
|
|
2280
|
+
clearTimeout(timer);
|
|
2281
|
+
if (code === 0) return resolve();
|
|
2282
|
+
const last = tail.split(/\r?\n/).filter((l) => l.trim()).slice(-3).join(" \xB7 ");
|
|
2283
|
+
reject(new Error(`npm ci failed (code ${code})${last ? `: ${last.slice(0, 300)}` : ""}`));
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
async function installDesk(dir, cb, install = npmCi) {
|
|
2288
|
+
const { log } = cb;
|
|
2289
|
+
try {
|
|
2290
|
+
const state = await readDeps(dir);
|
|
2291
|
+
const plan = deskDepsPlan(state);
|
|
2292
|
+
if (plan === "none") return "none";
|
|
2293
|
+
if (plan === "agent") {
|
|
2294
|
+
log(["No package-lock.json; leaving the dependencies to the agent."]);
|
|
2295
|
+
return "agent";
|
|
2296
|
+
}
|
|
2297
|
+
if (plan === "skip") {
|
|
2298
|
+
log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2299
|
+
return "skipped";
|
|
2300
|
+
}
|
|
2301
|
+
log(["Installing dependencies\u2026"]);
|
|
2302
|
+
cb.installing();
|
|
2303
|
+
await deskInstalls(async () => {
|
|
2304
|
+
log(["$ npm ci"]);
|
|
2305
|
+
await install(dir);
|
|
2306
|
+
});
|
|
2307
|
+
await writeMarker(dir, state.hash);
|
|
2308
|
+
log(["Dependencies installed."]);
|
|
2309
|
+
return "installed";
|
|
2310
|
+
} catch (err) {
|
|
2311
|
+
log([`\u26A0 Could not install the dependencies (${err.message.split(/\r?\n/)[0].slice(0, 300)}); the agent will run npm install itself.`]);
|
|
2312
|
+
return "failed";
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// server/previewRunner.ts
|
|
2317
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2318
|
+
import fs9 from "node:fs/promises";
|
|
2319
|
+
import net2 from "node:net";
|
|
2320
|
+
import path10 from "node:path";
|
|
2200
2321
|
var PREVIEW_SLUG = "preview";
|
|
2201
2322
|
var PREVIEW_BRANCH = "swarm-preview";
|
|
2202
2323
|
var PREVIEW_TMP = ".preview-tmp";
|
|
2203
|
-
var INSTALL_MARKER = ".cubefarm-preview-install";
|
|
2204
2324
|
var START_TIMEOUT_MS = 3 * 6e4;
|
|
2205
|
-
var
|
|
2206
|
-
var fillPlaceholders = (s, port, tmp) => s.replaceAll("{port}", String(port)).replaceAll("{tmp}", tmp);
|
|
2325
|
+
var fillPlaceholders = (s2, port, tmp) => s2.replaceAll("{port}", String(port)).replaceAll("{tmp}", tmp);
|
|
2207
2326
|
function previewEnv(extra, port, tmp) {
|
|
2208
2327
|
const env = {};
|
|
2209
2328
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -2232,35 +2351,19 @@ async function portOpen(port) {
|
|
|
2232
2351
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2233
2352
|
async function readJson(file) {
|
|
2234
2353
|
try {
|
|
2235
|
-
return JSON.parse(await
|
|
2354
|
+
return JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2236
2355
|
} catch {
|
|
2237
2356
|
return null;
|
|
2238
2357
|
}
|
|
2239
2358
|
}
|
|
2240
2359
|
function defaultCommand(pkg) {
|
|
2241
2360
|
const scripts = pkg?.scripts ?? {};
|
|
2242
|
-
const name = ["dev", "start", "preview"].find((
|
|
2361
|
+
const name = ["dev", "start", "preview"].find((s2) => typeof scripts[s2] === "string" && scripts[s2].trim());
|
|
2243
2362
|
if (!name) return null;
|
|
2244
2363
|
const script = scripts[name];
|
|
2245
2364
|
const vite = /^\s*vite(\s|$)/.test(script) && !/--port\b/.test(script);
|
|
2246
2365
|
return `npm run ${name}${vite ? " -- --port {port} --strictPort" : ""}`;
|
|
2247
2366
|
}
|
|
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
2367
|
var Stopped = class extends Error {
|
|
2265
2368
|
};
|
|
2266
2369
|
var RealPreview = class {
|
|
@@ -2293,9 +2396,9 @@ var RealPreview = class {
|
|
|
2293
2396
|
this.check();
|
|
2294
2397
|
await ensureLocalExcludes(job.fullName).catch(() => void 0);
|
|
2295
2398
|
cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
|
|
2296
|
-
const tmp =
|
|
2297
|
-
await
|
|
2298
|
-
const pkg = await readJson(
|
|
2399
|
+
const tmp = path10.join(wt, PREVIEW_TMP);
|
|
2400
|
+
await fs9.mkdir(tmp, { recursive: true });
|
|
2401
|
+
const pkg = await readJson(path10.join(wt, "package.json"));
|
|
2299
2402
|
const command = job.command ?? defaultCommand(pkg);
|
|
2300
2403
|
if (!command) {
|
|
2301
2404
|
this.stopped = true;
|
|
@@ -2334,15 +2437,12 @@ var RealPreview = class {
|
|
|
2334
2437
|
}
|
|
2335
2438
|
/** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
|
|
2336
2439
|
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) {
|
|
2440
|
+
const deps = await readDeps(wt);
|
|
2441
|
+
if (depsCurrent(deps)) {
|
|
2342
2442
|
this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2343
2443
|
return;
|
|
2344
2444
|
}
|
|
2345
|
-
const cmd = lock !== null ? "npm ci" : "npm install";
|
|
2445
|
+
const cmd = deps.lock !== null ? "npm ci" : "npm install";
|
|
2346
2446
|
this.cb.log([`$ ${cmd}`]);
|
|
2347
2447
|
const code = await Promise.race([this.spawn(cmd, wt, env), sleep2(INSTALL_TIMEOUT_MS).then(() => "timeout")]);
|
|
2348
2448
|
this.check();
|
|
@@ -2352,12 +2452,12 @@ var RealPreview = class {
|
|
|
2352
2452
|
}
|
|
2353
2453
|
if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
|
|
2354
2454
|
this.child = null;
|
|
2355
|
-
await
|
|
2455
|
+
await writeMarker(wt, deps.hash);
|
|
2356
2456
|
}
|
|
2357
2457
|
/** Run a command line through the platform shell; resolves with its exit code. */
|
|
2358
2458
|
spawn(cmd, cwd, env) {
|
|
2359
2459
|
this.check();
|
|
2360
|
-
const child =
|
|
2460
|
+
const child = spawn3(cmd, { cwd, env, shell: true, windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
2361
2461
|
this.child = child;
|
|
2362
2462
|
const onData = (buf) => {
|
|
2363
2463
|
const text = this.partial + buf.toString().replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
@@ -2383,29 +2483,29 @@ var RealPreview = class {
|
|
|
2383
2483
|
};
|
|
2384
2484
|
var realPreviews = {
|
|
2385
2485
|
start: (job, cb) => new RealPreview(job, cb),
|
|
2386
|
-
hasDefault: async (fullName) =>
|
|
2486
|
+
hasDefault: async (fullName) => fs9.access(path10.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
2387
2487
|
};
|
|
2388
2488
|
|
|
2389
2489
|
// server/officeUpdate.ts
|
|
2390
|
-
import
|
|
2391
|
-
import
|
|
2490
|
+
import fs10 from "node:fs/promises";
|
|
2491
|
+
import path11 from "node:path";
|
|
2392
2492
|
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
2393
2493
|
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
2394
2494
|
function underLauncher(proc = process) {
|
|
2395
2495
|
return typeof proc.send === "function" && proc.env.SWARM_LAUNCHER === "1";
|
|
2396
2496
|
}
|
|
2397
|
-
function drainDecision(
|
|
2497
|
+
function drainDecision(s2) {
|
|
2398
2498
|
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:
|
|
2499
|
+
if (s2.sent) return { state: "updating", drainingSince: s2.drainingSince, send: false, timedOut: false };
|
|
2500
|
+
if (s2.behind <= 0) return idle("none");
|
|
2501
|
+
if (!s2.launcher) return idle(s2.failed ? "failed" : "available");
|
|
2502
|
+
const postponed = s2.postponedUntil !== null && s2.now < s2.postponedUntil && s2.behind <= s2.postponedBehind;
|
|
2503
|
+
const failedHold = s2.failed !== null && s2.behind <= (s2.failedBehind ?? Infinity);
|
|
2504
|
+
const due = s2.requested || s2.autoUpdate && !postponed && !failedHold;
|
|
2505
|
+
if (!due) return idle(s2.failed ? "failed" : s2.autoUpdate && postponed ? "waiting" : "available");
|
|
2506
|
+
const since = s2.drainingSince ?? s2.now;
|
|
2507
|
+
const timedOut = s2.running > 0 && s2.now - since >= DRAIN_TIMEOUT_MS;
|
|
2508
|
+
return { state: "draining", drainingSince: since, send: s2.running === 0 || timedOut, timedOut };
|
|
2409
2509
|
}
|
|
2410
2510
|
function parseLastUpdate(raw) {
|
|
2411
2511
|
try {
|
|
@@ -2425,10 +2525,10 @@ function parseLastUpdate(raw) {
|
|
|
2425
2525
|
}
|
|
2426
2526
|
}
|
|
2427
2527
|
async function takeLastUpdate(home) {
|
|
2428
|
-
const file =
|
|
2429
|
-
const raw = await
|
|
2528
|
+
const file = path11.join(home, "last-update.json");
|
|
2529
|
+
const raw = await fs10.readFile(file, "utf8").catch(() => null);
|
|
2430
2530
|
if (raw === null) return null;
|
|
2431
|
-
await
|
|
2531
|
+
await fs10.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
2432
2532
|
return parseLastUpdate(raw);
|
|
2433
2533
|
}
|
|
2434
2534
|
function lastUpdateMessage(u, commits) {
|
|
@@ -2436,8 +2536,8 @@ function lastUpdateMessage(u, commits) {
|
|
|
2436
2536
|
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
2437
2537
|
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
2438
2538
|
}
|
|
2439
|
-
var OFFICE_DIR =
|
|
2440
|
-
var isSamePath = (a, b) => process.platform === "win32" ?
|
|
2539
|
+
var OFFICE_DIR = path11.resolve(import.meta.dirname, "..");
|
|
2540
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path11.resolve(a).toLowerCase() === path11.resolve(b).toLowerCase() : path11.resolve(a) === path11.resolve(b);
|
|
2441
2541
|
var realOffice = {
|
|
2442
2542
|
launcher: underLauncher(),
|
|
2443
2543
|
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
@@ -2451,6 +2551,340 @@ var realOffice = {
|
|
|
2451
2551
|
}
|
|
2452
2552
|
};
|
|
2453
2553
|
|
|
2554
|
+
// server/voice.ts
|
|
2555
|
+
import crypto5 from "node:crypto";
|
|
2556
|
+
import fs11 from "node:fs/promises";
|
|
2557
|
+
import path12 from "node:path";
|
|
2558
|
+
|
|
2559
|
+
// shared/speech.ts
|
|
2560
|
+
var SPEECH_MAX_CHARS = 1500;
|
|
2561
|
+
var SAMPLE_LINE = "Hi! This is how I'll sound when I message you from the office.";
|
|
2562
|
+
var EMOJI = /[\p{Extended_Pictographic}\p{Regional_Indicator}\u{1F3FB}-\u{1F3FF}︎️⃣]/gu;
|
|
2563
|
+
var URL2 = /\b(?:https?:\/\/|www\.)[^\s<>()]+[^\s<>().,;:!?'"]/gi;
|
|
2564
|
+
function speechText(text, max = SPEECH_MAX_CHARS) {
|
|
2565
|
+
let s2 = text.replace(/\r\n?/g, "\n");
|
|
2566
|
+
s2 = s2.replace(/```[\s\S]*?(```|$)/g, "\n");
|
|
2567
|
+
s2 = s2.replace(/`([^`\n]*)`/g, "$1");
|
|
2568
|
+
s2 = s2.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
2569
|
+
s2 = s2.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
2570
|
+
s2 = s2.replace(/<(https?:[^>\s]+)>/g, "$1");
|
|
2571
|
+
s2 = s2.replace(/<\/?[a-z][^>]*>/gi, "");
|
|
2572
|
+
s2 = s2.replace(URL2, "a link");
|
|
2573
|
+
s2 = s2.replace(EMOJI, "");
|
|
2574
|
+
s2 = s2.replace(/(\*\*|__)(.+?)\1/g, "$2");
|
|
2575
|
+
s2 = s2.replace(/(^|[^\w*])\*(?!\s)([^*\n]+?)\*(?!\w)/g, "$1$2");
|
|
2576
|
+
s2 = s2.replace(/(^|[^\w])_(?!\s)([^_\n]+?)_(?!\w)/g, "$1$2");
|
|
2577
|
+
s2 = s2.replace(/~~(.+?)~~/g, "$1");
|
|
2578
|
+
s2 = s2.replace(/#(\d+)\b/g, "number $1");
|
|
2579
|
+
const lines = [];
|
|
2580
|
+
for (let line of s2.split("\n")) {
|
|
2581
|
+
line = line.replace(/^\s*#{1,6}\s+/, "").replace(/^\s*>+\s?/, "").replace(/^\s*[-*+•]\s+/, "").replace(/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/, "").replace(/^\s*([-*_]\s*){3,}$/, "").replace(/\s*\|\s*/g, ", ").replace(/^[,\s]+|[,\s]+$/g, "").replace(/\s+/g, " ").trim();
|
|
2582
|
+
if (!line) continue;
|
|
2583
|
+
lines.push(/[.!?:;,…]$/.test(line) ? line : `${line}.`);
|
|
2584
|
+
}
|
|
2585
|
+
return cut(lines.join(" ").replace(/\s+([.,!?;:])/g, "$1"), max);
|
|
2586
|
+
}
|
|
2587
|
+
function cut(s2, max) {
|
|
2588
|
+
if (s2.length <= max) return s2;
|
|
2589
|
+
const head = s2.slice(0, max + 1);
|
|
2590
|
+
const ends = [...head.matchAll(/[.!?…](?=\s|$)/g)];
|
|
2591
|
+
const end = ends.length ? ends[ends.length - 1].index + 1 : -1;
|
|
2592
|
+
if (end >= max / 2) return s2.slice(0, end);
|
|
2593
|
+
const space = s2.lastIndexOf(" ", max);
|
|
2594
|
+
return `${s2.slice(0, space > 0 ? space : max).replace(/[,;:\s]+$/, "")}\u2026`;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// server/httpError.ts
|
|
2598
|
+
var HttpError = class extends Error {
|
|
2599
|
+
constructor(status, message) {
|
|
2600
|
+
super(message);
|
|
2601
|
+
this.status = status;
|
|
2602
|
+
}
|
|
2603
|
+
status;
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// server/voice.ts
|
|
2607
|
+
var DEFAULT_VOICE_MODEL = "eleven_flash_v2_5";
|
|
2608
|
+
var VOICES_TTL_MS = 10 * 6e4;
|
|
2609
|
+
var SYNTH_TIMEOUT_MS = 2e4;
|
|
2610
|
+
var CACHE_MAX_FILES = 200;
|
|
2611
|
+
var CACHE_MAX_AGE_MS = 7 * 24 * 60 * 6e4;
|
|
2612
|
+
var KEY_REJECTED_NOTE = "\u{1F507} ElevenLabs rejected the key, so messages aren't read aloud until you set a new one in the voice settings.";
|
|
2613
|
+
var voice = (id, name, description, accent = "") => ({
|
|
2614
|
+
id,
|
|
2615
|
+
name,
|
|
2616
|
+
category: "library",
|
|
2617
|
+
labels: { accent, gender: "", age: "", description, use_case: "conversational" },
|
|
2618
|
+
previewUrl: null,
|
|
2619
|
+
recommended: true
|
|
2620
|
+
});
|
|
2621
|
+
var RECOMMENDED_VOICES = [
|
|
2622
|
+
voice("OZ0L6eISlOejga3XjDFt", "Talia", "warm soft guide"),
|
|
2623
|
+
voice("l7kNoIfnJKPg7779LI2t", "Eddie", "helpful and comforting"),
|
|
2624
|
+
voice("AaOhDHYJ1XLZk74lXhdE", "Caleb", "trusted guide"),
|
|
2625
|
+
voice("gOupLcAkjEnguROwi4oS", "Darian", "warm grounded storyteller"),
|
|
2626
|
+
voice("BFd5oBc2DDna33pSi4Gf", "Alicia", "polished global anchor"),
|
|
2627
|
+
voice("QtY3JBOUKEB5xzrRfOKc", "Maisie", "friendly casual neighbor"),
|
|
2628
|
+
voice("FrS6cKLB1wg4WYgPa9GW", "Wyatt", "seasoned mentor"),
|
|
2629
|
+
voice("6WwXjDDEMyNmFG95zycZ", "Eldrin", "crisp baritone", "british")
|
|
2630
|
+
];
|
|
2631
|
+
var DEFAULT_VOICE = {
|
|
2632
|
+
provider: "off",
|
|
2633
|
+
voiceId: RECOMMENDED_VOICES[0].id,
|
|
2634
|
+
voiceName: RECOMMENDED_VOICES[0].name,
|
|
2635
|
+
model: DEFAULT_VOICE_MODEL,
|
|
2636
|
+
speakOffice: false
|
|
2637
|
+
};
|
|
2638
|
+
var VOICE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
2639
|
+
var MODEL_ID = /^[a-z0-9_.-]{1,64}$/;
|
|
2640
|
+
function voiceSettings(base, patch) {
|
|
2641
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
2642
|
+
const out = { ...base };
|
|
2643
|
+
if (p.provider === "off" || p.provider === "browser" || p.provider === "elevenlabs") out.provider = p.provider;
|
|
2644
|
+
if (typeof p.voiceId === "string" && p.voiceId.trim() && p.voiceId.length <= 200) out.voiceId = p.voiceId.trim();
|
|
2645
|
+
if (typeof p.voiceName === "string") out.voiceName = p.voiceName.trim().slice(0, 80);
|
|
2646
|
+
if (typeof p.model === "string" && MODEL_ID.test(p.model.trim())) out.model = p.model.trim();
|
|
2647
|
+
if (typeof p.speakOffice === "boolean") out.speakOffice = p.speakOffice;
|
|
2648
|
+
return out;
|
|
2649
|
+
}
|
|
2650
|
+
function cacheToPrune(files, now2, max = CACHE_MAX_FILES, maxAgeMs = CACHE_MAX_AGE_MS) {
|
|
2651
|
+
const fresh = files.filter((f) => now2 - f.mtimeMs <= maxAgeMs).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2652
|
+
const keep = new Set(fresh.slice(0, max).map((f) => f.name));
|
|
2653
|
+
return files.filter((f) => !keep.has(f.name)).map((f) => f.name);
|
|
2654
|
+
}
|
|
2655
|
+
function speaks(m, s2) {
|
|
2656
|
+
return m.from === "ceo" || m.from === "office" && s2.speakOffice;
|
|
2657
|
+
}
|
|
2658
|
+
var VoiceApiError = class extends Error {
|
|
2659
|
+
constructor(status, message) {
|
|
2660
|
+
super(message);
|
|
2661
|
+
this.status = status;
|
|
2662
|
+
}
|
|
2663
|
+
status;
|
|
2664
|
+
};
|
|
2665
|
+
var Voice = class {
|
|
2666
|
+
// secrets-file writes, one after another
|
|
2667
|
+
constructor(deps) {
|
|
2668
|
+
this.deps = deps;
|
|
2669
|
+
}
|
|
2670
|
+
deps;
|
|
2671
|
+
key = "";
|
|
2672
|
+
rejected = false;
|
|
2673
|
+
// ElevenLabs said 401: no more calls until the key changes
|
|
2674
|
+
voicesCache = null;
|
|
2675
|
+
making = /* @__PURE__ */ new Map();
|
|
2676
|
+
queue = Promise.resolve();
|
|
2677
|
+
writing = Promise.resolve();
|
|
2678
|
+
async init() {
|
|
2679
|
+
const s2 = await this.readSecrets();
|
|
2680
|
+
this.key = typeof s2.elevenlabsKey === "string" ? s2.elevenlabsKey : "";
|
|
2681
|
+
this.rejected = !!this.key && s2.elevenlabsKeyRejected === true;
|
|
2682
|
+
await this.prune().catch(() => void 0);
|
|
2683
|
+
}
|
|
2684
|
+
keyView() {
|
|
2685
|
+
return { voiceKeySet: !!this.key, voiceKeyHint: this.key.slice(-4) };
|
|
2686
|
+
}
|
|
2687
|
+
/** PUT /api/voice/key: '' removes the key; anything else must pass one cheap ElevenLabs call first. */
|
|
2688
|
+
async setKey(raw) {
|
|
2689
|
+
const key = raw.trim();
|
|
2690
|
+
if (key) {
|
|
2691
|
+
if (key.length > 200 || /\s/.test(key)) throw new HttpError(400, "That doesn't look like an ElevenLabs API key.");
|
|
2692
|
+
try {
|
|
2693
|
+
await this.deps.api.checkKey(key);
|
|
2694
|
+
} catch (err) {
|
|
2695
|
+
if (err instanceof VoiceApiError && err.status === 401) throw new HttpError(400, "ElevenLabs rejected that key. Copy it again from elevenlabs.io \u2192 Developers \u2192 API Keys.");
|
|
2696
|
+
if (err instanceof VoiceApiError && err.status === 403) throw new HttpError(400, "That key may not read voices. Give it the Voices (read) and Text to Speech permissions on elevenlabs.io.");
|
|
2697
|
+
throw new HttpError(502, `Couldn't check the key with ElevenLabs: ${err.message}`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
this.key = key;
|
|
2701
|
+
this.rejected = false;
|
|
2702
|
+
this.voicesCache = null;
|
|
2703
|
+
await this.writeSecrets({ elevenlabsKey: key || void 0, elevenlabsKeyRejected: void 0 });
|
|
2704
|
+
const view2 = this.keyView();
|
|
2705
|
+
this.deps.keyChanged(view2);
|
|
2706
|
+
return view2;
|
|
2707
|
+
}
|
|
2708
|
+
/** GET /api/voice/voices: the account's voices plus the shortlist, shortlist first. Cached for 10 minutes. */
|
|
2709
|
+
async voices() {
|
|
2710
|
+
this.requireKey();
|
|
2711
|
+
const now2 = this.now();
|
|
2712
|
+
if (this.voicesCache && now2 - this.voicesCache.at < VOICES_TTL_MS) return this.voicesCache.list;
|
|
2713
|
+
const key = this.key;
|
|
2714
|
+
const mine = await this.deps.api.listVoices(key).catch((err) => this.failed(err, key, "list the voices"));
|
|
2715
|
+
const recommended = new Map(RECOMMENDED_VOICES.map((v) => [v.id, v]));
|
|
2716
|
+
const list = [
|
|
2717
|
+
...RECOMMENDED_VOICES.map((r) => {
|
|
2718
|
+
const own = mine.find((v) => v.id === r.id);
|
|
2719
|
+
return own ? { ...own, labels: fill(own.labels, r.labels), recommended: true } : r;
|
|
2720
|
+
}),
|
|
2721
|
+
...mine.filter((v) => !recommended.has(v.id)).map((v) => ({ ...v, recommended: false })).sort((a, b) => a.name.localeCompare(b.name))
|
|
2722
|
+
];
|
|
2723
|
+
if (this.key === key) this.voicesCache = { at: now2, list };
|
|
2724
|
+
return list;
|
|
2725
|
+
}
|
|
2726
|
+
/** GET /api/voice/messages/:id: a phone message as mp3, made once and cached. */
|
|
2727
|
+
async messageAudio(id) {
|
|
2728
|
+
const s2 = this.deps.settings();
|
|
2729
|
+
if (s2.provider !== "elevenlabs") throw new HttpError(404, "Messages aren't read by ElevenLabs right now.");
|
|
2730
|
+
const m = this.deps.message(id);
|
|
2731
|
+
if (!m || !speaks(m, s2)) throw new HttpError(404, `No spoken message ${id}`);
|
|
2732
|
+
const text = speechText(m.text);
|
|
2733
|
+
if (!text) throw new HttpError(404, `Message ${id} has nothing to say aloud`);
|
|
2734
|
+
return this.audio(String(m.id), s2.voiceId, s2.model, text);
|
|
2735
|
+
}
|
|
2736
|
+
/** GET /api/voice/sample: the settings' Test button, in `voiceId` (default: the chosen voice). */
|
|
2737
|
+
async sampleAudio(voiceId) {
|
|
2738
|
+
const s2 = this.deps.settings();
|
|
2739
|
+
return this.audio("sample", voiceId || s2.voiceId, s2.model, SAMPLE_LINE);
|
|
2740
|
+
}
|
|
2741
|
+
async audio(name, voiceId, model, text) {
|
|
2742
|
+
if (!VOICE_ID.test(voiceId)) throw new HttpError(400, "Pick an ElevenLabs voice first.");
|
|
2743
|
+
const hash = crypto5.createHash("sha256").update(`${voiceId}
|
|
2744
|
+
${model}
|
|
2745
|
+
${text}`).digest("hex").slice(0, 12);
|
|
2746
|
+
const file = path12.join(this.deps.cacheDir, `${name}-${hash}.mp3`);
|
|
2747
|
+
const cached = await fs11.readFile(file).catch(() => null);
|
|
2748
|
+
if (cached) return cached;
|
|
2749
|
+
this.requireKey();
|
|
2750
|
+
const busy = this.making.get(file);
|
|
2751
|
+
if (busy) return busy;
|
|
2752
|
+
const made = this.oneAtATime(() => this.synthesize(file, { voiceId, model, text })).finally(() => this.making.delete(file));
|
|
2753
|
+
this.making.set(file, made);
|
|
2754
|
+
return made;
|
|
2755
|
+
}
|
|
2756
|
+
oneAtATime(fn) {
|
|
2757
|
+
const run2 = this.queue.then(fn, fn);
|
|
2758
|
+
this.queue = run2.catch(() => void 0);
|
|
2759
|
+
return run2;
|
|
2760
|
+
}
|
|
2761
|
+
async synthesize(file, req) {
|
|
2762
|
+
this.requireKey();
|
|
2763
|
+
const key = this.key;
|
|
2764
|
+
const audio = await this.deps.api.synthesize(key, req, AbortSignal.timeout(SYNTH_TIMEOUT_MS)).catch((err) => this.failed(err, key, "speak the message"));
|
|
2765
|
+
await fs11.mkdir(this.deps.cacheDir, { recursive: true });
|
|
2766
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2767
|
+
await fs11.writeFile(tmp, audio);
|
|
2768
|
+
await fs11.rename(tmp, file);
|
|
2769
|
+
await this.prune().catch(() => void 0);
|
|
2770
|
+
return audio;
|
|
2771
|
+
}
|
|
2772
|
+
/** An ElevenLabs failure as a short 502. A 401 also stops every call until the key changes, and says so once. */
|
|
2773
|
+
async failed(err, key, what) {
|
|
2774
|
+
if (err instanceof VoiceApiError && err.status === 401 && key === this.key && !this.rejected) {
|
|
2775
|
+
this.rejected = true;
|
|
2776
|
+
this.deps.officeNote(KEY_REJECTED_NOTE);
|
|
2777
|
+
await this.writeSecrets({ elevenlabsKeyRejected: true }).catch(() => void 0);
|
|
2778
|
+
}
|
|
2779
|
+
if (err instanceof VoiceApiError && err.status === 401) throw new HttpError(502, "ElevenLabs rejected the key.");
|
|
2780
|
+
throw new HttpError(502, `ElevenLabs couldn't ${what}: ${err.message}`.slice(0, 240));
|
|
2781
|
+
}
|
|
2782
|
+
requireKey() {
|
|
2783
|
+
if (!this.key) throw new HttpError(409, "Add an ElevenLabs API key first.");
|
|
2784
|
+
if (this.rejected) throw new HttpError(502, "ElevenLabs rejected the key. Set a new one in the voice settings.");
|
|
2785
|
+
}
|
|
2786
|
+
async prune() {
|
|
2787
|
+
const names = (await fs11.readdir(this.deps.cacheDir).catch(() => [])).filter((n) => n.endsWith(".mp3"));
|
|
2788
|
+
const files = await Promise.all(names.map(async (name) => ({ name, mtimeMs: (await fs11.stat(path12.join(this.deps.cacheDir, name)).catch(() => null))?.mtimeMs ?? 0 })));
|
|
2789
|
+
for (const name of cacheToPrune(files, this.now())) await fs11.rm(path12.join(this.deps.cacheDir, name), { force: true, maxRetries: 3 });
|
|
2790
|
+
}
|
|
2791
|
+
now() {
|
|
2792
|
+
return this.deps.now?.() ?? Date.now();
|
|
2793
|
+
}
|
|
2794
|
+
async readSecrets() {
|
|
2795
|
+
try {
|
|
2796
|
+
const s2 = JSON.parse(await fs11.readFile(this.deps.secretsFile, "utf8"));
|
|
2797
|
+
return s2 && typeof s2 === "object" ? s2 : {};
|
|
2798
|
+
} catch {
|
|
2799
|
+
return {};
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
/** Merge into the secrets file (other secrets stay), owner-only where the OS has file modes. */
|
|
2803
|
+
writeSecrets(patch) {
|
|
2804
|
+
const write = this.writing.then(() => this.mergeSecrets(patch));
|
|
2805
|
+
this.writing = write.catch(() => void 0);
|
|
2806
|
+
return write;
|
|
2807
|
+
}
|
|
2808
|
+
async mergeSecrets(patch) {
|
|
2809
|
+
const next = { ...await this.readSecrets(), ...patch };
|
|
2810
|
+
for (const k of Object.keys(next)) if (next[k] === void 0) delete next[k];
|
|
2811
|
+
await fs11.mkdir(path12.dirname(this.deps.secretsFile), { recursive: true });
|
|
2812
|
+
const tmp = `${this.deps.secretsFile}.tmp`;
|
|
2813
|
+
await fs11.writeFile(tmp, JSON.stringify(next, null, 2), { mode: 384 });
|
|
2814
|
+
await fs11.rename(tmp, this.deps.secretsFile);
|
|
2815
|
+
await fs11.chmod(this.deps.secretsFile, 384).catch(() => void 0);
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
function fill(own, ours) {
|
|
2819
|
+
return { ...own, description: own.description || ours.description };
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// server/elevenlabs.ts
|
|
2823
|
+
var API = "https://api.elevenlabs.io";
|
|
2824
|
+
var CHECK_TIMEOUT_MS = 1e4;
|
|
2825
|
+
var MAX_VOICE_PAGES = 5;
|
|
2826
|
+
async function call(key, pathname, init, signal) {
|
|
2827
|
+
let res;
|
|
2828
|
+
try {
|
|
2829
|
+
res = await fetch(`${API}${pathname}`, { ...init, signal, headers: { ...init.headers, "xi-api-key": key } });
|
|
2830
|
+
} catch (err) {
|
|
2831
|
+
throw new VoiceApiError(0, signal.aborted ? "it took too long to answer" : `it couldn't be reached (${err.message})`);
|
|
2832
|
+
}
|
|
2833
|
+
if (!res.ok) throw new VoiceApiError(res.status, await reason(res));
|
|
2834
|
+
return res;
|
|
2835
|
+
}
|
|
2836
|
+
async function reason(res) {
|
|
2837
|
+
const body2 = await res.text().catch(() => "");
|
|
2838
|
+
let text = "";
|
|
2839
|
+
try {
|
|
2840
|
+
const d = JSON.parse(body2).detail;
|
|
2841
|
+
if (typeof d === "string") text = d;
|
|
2842
|
+
else if (Array.isArray(d)) text = d.map((x) => x?.msg).filter(Boolean).join("; ");
|
|
2843
|
+
else if (d && typeof d === "object") text = String(d.message ?? d.status ?? "");
|
|
2844
|
+
} catch {
|
|
2845
|
+
}
|
|
2846
|
+
return `${res.status}${text ? `: ${text}` : ""}`.slice(0, 200);
|
|
2847
|
+
}
|
|
2848
|
+
var s = (v) => typeof v === "string" ? v : "";
|
|
2849
|
+
function voiceOption(v) {
|
|
2850
|
+
const l = v.labels ?? {};
|
|
2851
|
+
return {
|
|
2852
|
+
id: s(v.voice_id),
|
|
2853
|
+
name: s(v.name),
|
|
2854
|
+
category: s(v.category),
|
|
2855
|
+
labels: { accent: s(l.accent), gender: s(l.gender), age: s(l.age), description: s(l.description) || s(l.descriptive), use_case: s(l.use_case) || s(l["use case"]) },
|
|
2856
|
+
previewUrl: s(v.preview_url) || null
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
var elevenLabs = {
|
|
2860
|
+
// The cheapest call a voices-reading key can make: one voice.
|
|
2861
|
+
async checkKey(key) {
|
|
2862
|
+
await call(key, "/v2/voices?page_size=1&include_total_count=false", {}, AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2863
|
+
},
|
|
2864
|
+
async listVoices(key) {
|
|
2865
|
+
const out = [];
|
|
2866
|
+
let token = "";
|
|
2867
|
+
for (let page = 0; page < MAX_VOICE_PAGES; page++) {
|
|
2868
|
+
const q = new URLSearchParams({ page_size: "100", include_total_count: "false", ...token ? { next_page_token: token } : {} });
|
|
2869
|
+
const res = await call(key, `/v2/voices?${q}`, {}, AbortSignal.timeout(CHECK_TIMEOUT_MS));
|
|
2870
|
+
const body2 = await res.json();
|
|
2871
|
+
for (const v of body2.voices ?? []) if (s(v.voice_id)) out.push(voiceOption(v));
|
|
2872
|
+
token = s(body2.next_page_token);
|
|
2873
|
+
if (!body2.has_more || !token) break;
|
|
2874
|
+
}
|
|
2875
|
+
return out;
|
|
2876
|
+
},
|
|
2877
|
+
async synthesize(key, { voiceId, model, text }, signal) {
|
|
2878
|
+
const res = await call(
|
|
2879
|
+
key,
|
|
2880
|
+
`/v1/text-to-speech/${encodeURIComponent(voiceId)}?output_format=mp3_44100_128`,
|
|
2881
|
+
{ method: "POST", headers: { "Content-Type": "application/json", Accept: "audio/mpeg" }, body: JSON.stringify({ text, model_id: model }) },
|
|
2882
|
+
signal
|
|
2883
|
+
);
|
|
2884
|
+
return Buffer.from(await res.arrayBuffer());
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2454
2888
|
// server/backend.ts
|
|
2455
2889
|
var realBackend = {
|
|
2456
2890
|
demo: false,
|
|
@@ -2481,6 +2915,7 @@ var realBackend = {
|
|
|
2481
2915
|
mainDir,
|
|
2482
2916
|
deskDir,
|
|
2483
2917
|
prepareDesk,
|
|
2918
|
+
installDeps: (dir, cb) => installDesk(dir, cb),
|
|
2484
2919
|
removeDesk,
|
|
2485
2920
|
sweepDesks,
|
|
2486
2921
|
releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
|
|
@@ -2491,22 +2926,23 @@ var realBackend = {
|
|
|
2491
2926
|
releaseClis,
|
|
2492
2927
|
detectClis,
|
|
2493
2928
|
previews: realPreviews,
|
|
2494
|
-
office: realOffice
|
|
2929
|
+
office: realOffice,
|
|
2930
|
+
voice: elevenLabs
|
|
2495
2931
|
};
|
|
2496
2932
|
|
|
2497
2933
|
// server/demo.ts
|
|
2498
|
-
import
|
|
2934
|
+
import crypto6 from "node:crypto";
|
|
2499
2935
|
import http from "node:http";
|
|
2500
|
-
import
|
|
2936
|
+
import path13 from "node:path";
|
|
2501
2937
|
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() };
|
|
2938
|
+
function issue(n, title, body2, fullName, labels = []) {
|
|
2939
|
+
return { number: n, title, body: body2, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
2504
2940
|
}
|
|
2505
2941
|
var repos = /* @__PURE__ */ new Map();
|
|
2506
2942
|
var bareRepos = /* @__PURE__ */ new Set();
|
|
2507
2943
|
var mergedSinceSync = /* @__PURE__ */ new Map();
|
|
2508
2944
|
var closedIssues = /* @__PURE__ */ new Set();
|
|
2509
|
-
var fakeSha = () =>
|
|
2945
|
+
var fakeSha = () => crypto6.randomBytes(20).toString("hex");
|
|
2510
2946
|
function runChecks(pr, fail = Math.random() < 0.2) {
|
|
2511
2947
|
Object.assign(pr, { checks: "pending", pendingChecks: ["CI / build", "Vercel"], failedChecks: [] });
|
|
2512
2948
|
setTimeout(() => {
|
|
@@ -2519,7 +2955,7 @@ function runChecks(pr, fail = Math.random() < 0.2) {
|
|
|
2519
2955
|
}
|
|
2520
2956
|
function seed(fullName, description, titles) {
|
|
2521
2957
|
const r = { fullName, description, issues: [], pulls: [], nextNumber: 1 };
|
|
2522
|
-
for (const [title,
|
|
2958
|
+
for (const [title, body2] of titles) r.issues.push(issue(r.nextNumber++, title, body2, fullName));
|
|
2523
2959
|
repos.set(fullName, r);
|
|
2524
2960
|
}
|
|
2525
2961
|
seed("demo-co/pixel-todo", "A cheerful todo app", [
|
|
@@ -2536,7 +2972,7 @@ seed("demo-co/weather-api", "Tiny weather REST API", [
|
|
|
2536
2972
|
["OpenAPI spec", "Publish an OpenAPI 3.1 document at /openapi.json."]
|
|
2537
2973
|
]);
|
|
2538
2974
|
function screenshotSvg(title, url, hue) {
|
|
2539
|
-
const safe = (
|
|
2975
|
+
const safe = (s2) => s2.replace(/[<>&"]/g, "");
|
|
2540
2976
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="400" viewBox="0 0 640 400">
|
|
2541
2977
|
<rect width="640" height="400" fill="hsl(${hue},60%,97%)"/>
|
|
2542
2978
|
<rect width="640" height="56" fill="hsl(${hue},70%,55%)"/>
|
|
@@ -2550,7 +2986,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
2550
2986
|
</svg>`;
|
|
2551
2987
|
}
|
|
2552
2988
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
2553
|
-
const branch =
|
|
2989
|
+
const branch = path13.basename(opts.cwd);
|
|
2554
2990
|
const hue = issueNumber * 67 % 360;
|
|
2555
2991
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
2556
2992
|
const port = 5200 + issueNumber % 50;
|
|
@@ -2641,8 +3077,8 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2641
3077
|
{ kind: "system", text: ` cwd ${opts.cwd}` }
|
|
2642
3078
|
];
|
|
2643
3079
|
const checks = opts.prompt.match(/^GitHub checks right now: .*$/m)?.[0] ?? "GitHub checks right now: none";
|
|
2644
|
-
const
|
|
2645
|
-
const script = [header, ...
|
|
3080
|
+
const body2 = kind === "qa" ? qaScript(cb, number, title, round, checks) : kind === "fix" ? fixScript(number, pushes) : devScript(opts, cb, number, title);
|
|
3081
|
+
const script = [header, ...body2];
|
|
2646
3082
|
const finish = () => {
|
|
2647
3083
|
const costUsd = 0.3 + Math.random();
|
|
2648
3084
|
const turns = 15 + Math.floor(Math.random() * 20);
|
|
@@ -2697,7 +3133,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2697
3133
|
number: n,
|
|
2698
3134
|
title,
|
|
2699
3135
|
url,
|
|
2700
|
-
headRefName: `swarm/issue-${number}-${
|
|
3136
|
+
headRefName: `swarm/issue-${number}-${path13.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
2701
3137
|
state: "OPEN",
|
|
2702
3138
|
isDraft: false,
|
|
2703
3139
|
mergeable: "MERGEABLE",
|
|
@@ -2723,12 +3159,12 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2723
3159
|
const step = () => {
|
|
2724
3160
|
if (stopped) return;
|
|
2725
3161
|
if (i >= script.length) return finish();
|
|
2726
|
-
const
|
|
2727
|
-
if (typeof
|
|
3162
|
+
const s2 = script[i++];
|
|
3163
|
+
if (typeof s2 === "function") s2();
|
|
2728
3164
|
else {
|
|
2729
|
-
const tool2 =
|
|
3165
|
+
const tool2 = s2.find((e) => e.kind === "tool");
|
|
2730
3166
|
cb.tool(tool2?.tool ?? null);
|
|
2731
|
-
cb.log(
|
|
3167
|
+
cb.log(s2);
|
|
2732
3168
|
}
|
|
2733
3169
|
timers.push(setTimeout(step, 1600 + Math.random() * 3800));
|
|
2734
3170
|
};
|
|
@@ -2826,6 +3262,7 @@ function fakeUsageWarning(cb) {
|
|
|
2826
3262
|
}
|
|
2827
3263
|
function createDemoBackend() {
|
|
2828
3264
|
const deskRepo = /* @__PURE__ */ new Map();
|
|
3265
|
+
const installedDesks = /* @__PURE__ */ new Set();
|
|
2829
3266
|
const folders = /* @__PURE__ */ new Map();
|
|
2830
3267
|
const addFolder = (name, github, git2 = true) => folders.set(name, { name, path: `/demo/projects/${name}`, git: git2, github, modified: Date.now() - folders.size * 36e5 });
|
|
2831
3268
|
for (const r of repos.values()) addFolder(r.fullName.split("/")[1], r.fullName);
|
|
@@ -2867,11 +3304,11 @@ function createDemoBackend() {
|
|
|
2867
3304
|
},
|
|
2868
3305
|
listIssues: async (fullName) => [...repos.get(fullName)?.issues ?? []],
|
|
2869
3306
|
listPulls: async (fullName) => [...repos.get(fullName)?.pulls ?? []],
|
|
2870
|
-
createIssue: async (fullName, title,
|
|
3307
|
+
createIssue: async (fullName, title, body2, labels = []) => {
|
|
2871
3308
|
const r = repos.get(fullName);
|
|
2872
3309
|
if (!r) throw new Error("Unknown repo");
|
|
2873
3310
|
const n = r.nextNumber++;
|
|
2874
|
-
r.issues.push(issue(n, title,
|
|
3311
|
+
r.issues.push(issue(n, title, body2, fullName, labels));
|
|
2875
3312
|
return n;
|
|
2876
3313
|
},
|
|
2877
3314
|
issueState: async (fullName, number) => {
|
|
@@ -2959,6 +3396,18 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2959
3396
|
deskRepo.set(dir, fullName);
|
|
2960
3397
|
return dir;
|
|
2961
3398
|
},
|
|
3399
|
+
installDeps: async (dir, cb) => {
|
|
3400
|
+
if (installedDesks.has(dir)) {
|
|
3401
|
+
cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
3402
|
+
return "skipped";
|
|
3403
|
+
}
|
|
3404
|
+
cb.log(["Installing dependencies\u2026", "$ npm ci"]);
|
|
3405
|
+
cb.installing();
|
|
3406
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
3407
|
+
cb.log(["Dependencies installed."]);
|
|
3408
|
+
installedDesks.add(dir);
|
|
3409
|
+
return "installed";
|
|
3410
|
+
},
|
|
2962
3411
|
removeDesk: async () => void 0,
|
|
2963
3412
|
sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
|
|
2964
3413
|
releaseDesk: async () => void 0,
|
|
@@ -2973,7 +3422,8 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2973
3422
|
releaseClis: async () => void 0,
|
|
2974
3423
|
detectClis: async () => CLIS.map((c) => ({ id: c.id, label: c.label, installed: true, version: "demo", integrated: c.integrated })),
|
|
2975
3424
|
previews: demoPreviews,
|
|
2976
|
-
office: demoOffice
|
|
3425
|
+
office: demoOffice,
|
|
3426
|
+
voice: demoVoice
|
|
2977
3427
|
};
|
|
2978
3428
|
}
|
|
2979
3429
|
var OFFICE_REPO = "demo-co/pixel-todo";
|
|
@@ -3078,11 +3528,11 @@ var demoPreviews = {
|
|
|
3078
3528
|
async stop() {
|
|
3079
3529
|
stopped = true;
|
|
3080
3530
|
timers.forEach(clearTimeout);
|
|
3081
|
-
const
|
|
3531
|
+
const s2 = server2;
|
|
3082
3532
|
server2 = null;
|
|
3083
|
-
if (!
|
|
3084
|
-
|
|
3085
|
-
await new Promise((resolve) =>
|
|
3533
|
+
if (!s2?.listening) return;
|
|
3534
|
+
s2.closeAllConnections();
|
|
3535
|
+
await new Promise((resolve) => s2.close(() => resolve()));
|
|
3086
3536
|
}
|
|
3087
3537
|
};
|
|
3088
3538
|
}
|
|
@@ -3154,12 +3604,12 @@ function ceoSession(opts, cb) {
|
|
|
3154
3604
|
};
|
|
3155
3605
|
const status = async () => {
|
|
3156
3606
|
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
|
|
3607
|
+
const s2 = JSON.parse(await office.call("company_status", {}));
|
|
3608
|
+
const people = s2.floors.reduce((n, f) => n + f.team.length, 0);
|
|
3609
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3610
|
+
const free = s2.floors.reduce((n, f) => n + f.seats.dev.free + f.seats.qa.free, 0);
|
|
3611
|
+
cb.log([{ kind: "result", text: ` \u23BF ${s2.floors.length} floors \xB7 ${people} people \xB7 ${free} free seats \xB7 ${issues} open issues \xB7 ${s2.pendingProposals.length} proposals pending` }]);
|
|
3612
|
+
return s2;
|
|
3163
3613
|
};
|
|
3164
3614
|
const use = async (name, args) => {
|
|
3165
3615
|
await step([{ kind: "tool", tool: `mcp__office__${name}`, text: `\u23FA ${describeOfficeTool(name, args)}` }]);
|
|
@@ -3169,7 +3619,7 @@ function ceoSession(opts, cb) {
|
|
|
3169
3619
|
};
|
|
3170
3620
|
const read = (file, lines) => step([{ kind: "tool", tool: "Read", text: `\u23FA Read ${file}` }, { kind: "result", text: ` \u23BF Read ${lines} lines` }]);
|
|
3171
3621
|
const think = (text) => step([{ kind: "thinking", text: "\u273B Thinking\u2026" }, { kind: "text", text: `\u25CF ${text}` }], 1800);
|
|
3172
|
-
const short = (
|
|
3622
|
+
const short = (s2, n = 48) => s2.length > n ? `${s2.slice(0, n - 1).trim()}\u2026` : s2;
|
|
3173
3623
|
const planIssues = async (floor, mission, team) => {
|
|
3174
3624
|
const first = await use("file_issue", {
|
|
3175
3625
|
floor,
|
|
@@ -3212,8 +3662,8 @@ Acceptance criteria:
|
|
|
3212
3662
|
};
|
|
3213
3663
|
const scripts = {
|
|
3214
3664
|
async onboard(floor, fullName) {
|
|
3215
|
-
const
|
|
3216
|
-
const f =
|
|
3665
|
+
const s2 = await status();
|
|
3666
|
+
const f = s2.floors.find((x) => x.floor === floor);
|
|
3217
3667
|
if (!f) return "That floor has gone, so there was nothing to onboard.";
|
|
3218
3668
|
const p = PROFILES[fullName.split("/")[1] ?? ""] ?? GENERIC;
|
|
3219
3669
|
await read(`${f.repo}/README.md`, 48);
|
|
@@ -3245,8 +3695,8 @@ Acceptance criteria:
|
|
|
3245
3695
|
].filter(Boolean).join(" ");
|
|
3246
3696
|
},
|
|
3247
3697
|
async plan(floor, mission) {
|
|
3248
|
-
const
|
|
3249
|
-
const f =
|
|
3698
|
+
const s2 = await status();
|
|
3699
|
+
const f = s2.floors.find((x) => x.floor === floor);
|
|
3250
3700
|
if (!f) return "That floor has gone, so there was nothing to plan.";
|
|
3251
3701
|
await read(`${f.repo}/README.md`, 12);
|
|
3252
3702
|
await think("Foundation first, so the parallel work doesn't collide.");
|
|
@@ -3254,9 +3704,9 @@ Acceptance criteria:
|
|
|
3254
3704
|
return `I turned the brief into three issues on floor ${floor}. #${n} sets up the skeleton; the other two say "Depends on #${n}", so nobody starts them early.`;
|
|
3255
3705
|
},
|
|
3256
3706
|
async review() {
|
|
3257
|
-
const
|
|
3707
|
+
const s2 = await status();
|
|
3258
3708
|
await think("Checking each floor for idle people and stuck work.");
|
|
3259
|
-
for (const f of
|
|
3709
|
+
for (const f of s2.floors) {
|
|
3260
3710
|
const idle = f.team.filter((a) => a.role === "dev" && !a.specialty && (a.status === "idle" || a.status === "done"));
|
|
3261
3711
|
const devs = f.team.filter((a) => a.role === "dev").length;
|
|
3262
3712
|
if (devs >= 6 && idle.length >= 2 && f.backlog.length < devs) {
|
|
@@ -3264,26 +3714,26 @@ Acceptance criteria:
|
|
|
3264
3714
|
return `Floor ${f.floor} is overstaffed: ${devs} developers for ${f.backlog.length} open issues. I suggest letting ${idle[idle.length - 1].name} go; it's on your phone.`;
|
|
3265
3715
|
}
|
|
3266
3716
|
}
|
|
3267
|
-
for (const f of
|
|
3717
|
+
for (const f of s2.floors) {
|
|
3268
3718
|
const specialist = f.team.find((a) => a.role === "dev" && a.specialty);
|
|
3269
3719
|
const unrouted = f.backlog.find((i) => !i.specialty && !i.inProgress);
|
|
3270
3720
|
if (!specialist || !unrouted) continue;
|
|
3271
3721
|
const out = await use("route_issue", { floor: f.floor, number: unrouted.number, specialty: specialist.specialty });
|
|
3272
3722
|
if (!out.startsWith("Refused")) return `Floor ${f.floor}: #${unrouted.number} had no specialty, so I routed it to ${specialist.specialty}, ${specialist.name}'s lane.`;
|
|
3273
3723
|
}
|
|
3274
|
-
const issues =
|
|
3275
|
-
const prs =
|
|
3276
|
-
return `All ${
|
|
3724
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3725
|
+
const prs = s2.floors.reduce((n, f) => n + f.pullRequests.length, 0);
|
|
3726
|
+
return `All ${s2.floors.length} floors look healthy: ${issues} open issues and ${prs} pull requests in flight. No changes needed.`;
|
|
3277
3727
|
},
|
|
3278
3728
|
async chat(text) {
|
|
3279
|
-
const
|
|
3729
|
+
const s2 = await status();
|
|
3280
3730
|
await think("Reading your message.");
|
|
3281
|
-
const people =
|
|
3282
|
-
const issues =
|
|
3283
|
-
const pending2 =
|
|
3284
|
-
const rows =
|
|
3731
|
+
const people = s2.floors.reduce((n, f) => n + f.team.length, 0);
|
|
3732
|
+
const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
3733
|
+
const pending2 = s2.pendingProposals.length;
|
|
3734
|
+
const rows = s2.floors.map((f) => `| ${f.floor} | \`${f.repo.split("/").pop()}\` | ${f.team.length} | ${f.backlog.length} | ${f.pullRequests.length} |`);
|
|
3285
3735
|
return [
|
|
3286
|
-
`**Quick status:** ${
|
|
3736
|
+
`**Quick status:** ${s2.floors.length} floor${s2.floors.length === 1 ? "" : "s"}, ${people} people and ${issues} open issues.`,
|
|
3287
3737
|
"",
|
|
3288
3738
|
"- The team is *heads down* on the backlog",
|
|
3289
3739
|
`- ${pending2 ? `**${pending2}** proposal${pending2 === 1 ? " is" : "s are"} waiting for you in Hires` : "No hiring decisions waiting on you"}`,
|
|
@@ -3347,34 +3797,154 @@ Acceptance criteria:
|
|
|
3347
3797
|
}
|
|
3348
3798
|
};
|
|
3349
3799
|
}
|
|
3800
|
+
function demoChime(chars) {
|
|
3801
|
+
const rate = 8e3;
|
|
3802
|
+
const seconds = Math.min(0.4 + chars / 400, 2);
|
|
3803
|
+
const n = Math.round(rate * seconds);
|
|
3804
|
+
const wav = Buffer.alloc(44 + n);
|
|
3805
|
+
wav.write("RIFF", 0);
|
|
3806
|
+
wav.writeUInt32LE(36 + n, 4);
|
|
3807
|
+
wav.write("WAVEfmt ", 8);
|
|
3808
|
+
wav.writeUInt32LE(16, 16);
|
|
3809
|
+
wav.writeUInt16LE(1, 20);
|
|
3810
|
+
wav.writeUInt16LE(1, 22);
|
|
3811
|
+
wav.writeUInt32LE(rate, 24);
|
|
3812
|
+
wav.writeUInt32LE(rate, 28);
|
|
3813
|
+
wav.writeUInt16LE(1, 32);
|
|
3814
|
+
wav.writeUInt16LE(8, 34);
|
|
3815
|
+
wav.write("data", 36);
|
|
3816
|
+
wav.writeUInt32LE(n, 40);
|
|
3817
|
+
for (let i = 0; i < n; i++) {
|
|
3818
|
+
const t = i / rate;
|
|
3819
|
+
const f = t < seconds / 2 ? 660 : 880;
|
|
3820
|
+
const fade = Math.min(1, (seconds - t) * 8, t * 40);
|
|
3821
|
+
wav[44 + i] = Math.round(128 + 40 * fade * Math.sin(2 * Math.PI * f * t));
|
|
3822
|
+
}
|
|
3823
|
+
return wav;
|
|
3824
|
+
}
|
|
3825
|
+
var demoVoices = [
|
|
3826
|
+
{ id: "demoVoiceAvery00001", name: "Avery", category: "premade", labels: { accent: "american", gender: "female", age: "middle aged", description: "calm", use_case: "conversational" }, previewUrl: null },
|
|
3827
|
+
{ id: "demoVoiceBasil00002", name: "Basil", category: "premade", labels: { accent: "british", gender: "male", age: "middle aged", description: "warm", use_case: "narration" }, previewUrl: null },
|
|
3828
|
+
{ id: "demoVoiceCleo000003", name: "Cleo", category: "premade", labels: { accent: "australian", gender: "female", age: "young", description: "friendly", use_case: "conversational" }, previewUrl: null }
|
|
3829
|
+
];
|
|
3830
|
+
var demoVoice = {
|
|
3831
|
+
checkKey: async (key) => {
|
|
3832
|
+
if (/bad/i.test(key)) throw new VoiceApiError(401, "401: invalid_api_key (demo)");
|
|
3833
|
+
},
|
|
3834
|
+
listVoices: async () => demoVoices,
|
|
3835
|
+
synthesize: async (_key, { text }) => {
|
|
3836
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3837
|
+
return demoChime(text.length);
|
|
3838
|
+
}
|
|
3839
|
+
};
|
|
3840
|
+
|
|
3841
|
+
// server/screenReply.ts
|
|
3842
|
+
function screenStatus(shot) {
|
|
3843
|
+
if (shot === void 0) return 404;
|
|
3844
|
+
return shot ? 200 : 204;
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
// server/sendBack.ts
|
|
3848
|
+
import { z } from "zod";
|
|
3849
|
+
|
|
3850
|
+
// server/mergeGate.ts
|
|
3851
|
+
var MAX_MERGE_FIXES = 3;
|
|
3852
|
+
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
3853
|
+
var MERGE_RETRY_MS = 10 * 6e4;
|
|
3854
|
+
function mergeStep(pr, rec, now2, opts) {
|
|
3855
|
+
if (pr.isDraft) return { do: "wait", note: "a draft: waiting", set: {} };
|
|
3856
|
+
if (!opts.detailed && (pr.mergeable === "UNKNOWN" || pr.mergeState === "UNKNOWN")) return { do: "details", set: {} };
|
|
3857
|
+
const set = {};
|
|
3858
|
+
if (rec.passedSha == null) set.passedSha = pr.headSha;
|
|
3859
|
+
if (pr.headSha !== (set.passedSha ?? rec.passedSha)) return { do: "requeue", set };
|
|
3860
|
+
const needsHuman = rec.mergeFixes >= MAX_MERGE_FIXES;
|
|
3861
|
+
if (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY") {
|
|
3862
|
+
return { do: "send-back", reason: "conflict", instructions: `It conflicts with ${opts.base}.`, needsHuman, set };
|
|
3863
|
+
}
|
|
3864
|
+
if (pr.checks === "failing") {
|
|
3865
|
+
const instructions = pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n");
|
|
3866
|
+
return { do: "send-back", reason: "checks", instructions, needsHuman, set };
|
|
3867
|
+
}
|
|
3868
|
+
if (pr.checks === "pending") {
|
|
3869
|
+
const since = rec.pendingSince ?? now2;
|
|
3870
|
+
if (rec.pendingSince == null) set.pendingSince = since;
|
|
3871
|
+
const alert = now2 - since > CHECKS_ALERT_MS && !rec.alerted;
|
|
3872
|
+
if (alert) set.alerted = true;
|
|
3873
|
+
return { do: "wait", note: `waiting for checks: ${pr.pendingChecks.join(", ") || "running"}`, alert, set };
|
|
3874
|
+
}
|
|
3875
|
+
set.pendingSince = null;
|
|
3876
|
+
if (pr.mergeable === "UNKNOWN") return { do: "wait", note: "GitHub is checking it can merge", set };
|
|
3877
|
+
if (pr.mergeState === "BEHIND") return { do: "update-branch", set };
|
|
3878
|
+
if (rec.mergeRetryAt && now2 < rec.mergeRetryAt) return { do: "wait", set };
|
|
3879
|
+
return { do: "merge", set };
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// server/qaOutcome.ts
|
|
3883
|
+
var MAX_QA_ROUNDS = 3;
|
|
3884
|
+
function lastQaRound(rec) {
|
|
3885
|
+
return rec.round - rec.retests >= MAX_QA_ROUNDS;
|
|
3886
|
+
}
|
|
3887
|
+
function qaOutcome(pass, rec, pr) {
|
|
3888
|
+
if (pass) return "passed";
|
|
3889
|
+
if (!lastQaRound(rec)) return "failed";
|
|
3890
|
+
const conflicting = pr != null && (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY");
|
|
3891
|
+
return conflicting && rec.mergeFixes < MAX_MERGE_FIXES ? "conflict" : "needs-human";
|
|
3892
|
+
}
|
|
3893
|
+
function conflictFixInstructions(qaInstructions2, base) {
|
|
3894
|
+
const merge = `It also conflicts with ${base}: bring it up to date (git fetch origin && git merge origin/${base}) and resolve the conflicts so both this change and the newly merged work keep working.`;
|
|
3895
|
+
return qaInstructions2.trim() ? `${qaInstructions2.trim()}
|
|
3896
|
+
|
|
3897
|
+
${merge}` : merge;
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// server/sendBack.ts
|
|
3901
|
+
var MAX_SEND_BACK_NOTE = 1e3;
|
|
3902
|
+
var body = z.object({ note: z.string().max(MAX_SEND_BACK_NOTE).optional() });
|
|
3903
|
+
function parseSendBackNote(input) {
|
|
3904
|
+
const parsed = body.safeParse(input ?? {});
|
|
3905
|
+
if (!parsed.success) throw new HttpError(400, `note must be text of at most ${MAX_SEND_BACK_NOTE} characters`);
|
|
3906
|
+
return parsed.data.note?.trim() || void 0;
|
|
3907
|
+
}
|
|
3908
|
+
function sendBackPatch(prNumber, pr, rec, base, note) {
|
|
3909
|
+
if (!pr || pr.state !== "OPEN") throw new HttpError(404, `PR #${prNumber} is not open`);
|
|
3910
|
+
if (rec?.status === "testing" || rec?.status === "fixing") throw new HttpError(409, `PR #${prNumber} is already ${rec.status}`);
|
|
3911
|
+
if (!rec || rec.status !== "needs-human" && rec.status !== "failed") {
|
|
3912
|
+
throw new HttpError(409, `PR #${prNumber} isn't waiting on you: only a PR that failed QA or needs a human can go back to a developer`);
|
|
3913
|
+
}
|
|
3914
|
+
const conflict = pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY";
|
|
3915
|
+
const findings = [rec.fixInstructions?.trim(), note?.trim() && `From the manager: ${note.trim()}`].filter(Boolean).join("\n\n");
|
|
3916
|
+
const addMerge = conflict && !rec.passedSha && !findings.includes(`conflicts with ${base}`);
|
|
3917
|
+
const fixInstructions = (addMerge ? conflictFixInstructions(findings, base) : findings) || null;
|
|
3918
|
+
return { status: "failed", fixReason: conflict ? "conflict" : "qa", fixInstructions, sessionFailures: 0, retests: rec.retests + 1, mergeNote: null };
|
|
3919
|
+
}
|
|
3350
3920
|
|
|
3351
3921
|
// server/swarm.ts
|
|
3352
|
-
import
|
|
3353
|
-
import
|
|
3354
|
-
import
|
|
3922
|
+
import fs13 from "node:fs/promises";
|
|
3923
|
+
import path15 from "node:path";
|
|
3924
|
+
import crypto7 from "node:crypto";
|
|
3355
3925
|
|
|
3356
3926
|
// server/ceo.ts
|
|
3357
3927
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
3358
|
-
import { z } from "zod";
|
|
3928
|
+
import { z as z2 } from "zod";
|
|
3359
3929
|
|
|
3360
3930
|
// shared/issues.ts
|
|
3361
3931
|
function issueSpecialty(labels) {
|
|
3362
3932
|
const l = labels.find((x) => /^swarm:/i.test(x) && !/^swarm:skip$/i.test(x));
|
|
3363
3933
|
return l ? l.slice(6).toLowerCase() : "";
|
|
3364
3934
|
}
|
|
3365
|
-
function blockers(
|
|
3935
|
+
function blockers(body2, open) {
|
|
3366
3936
|
const out = /* @__PURE__ */ new Set();
|
|
3367
|
-
for (const m of (
|
|
3937
|
+
for (const m of (body2 ?? "").matchAll(/\b(?:depends\s+on|blocked\s+by)\s*:?\s*((?:#\d+(?:\s*(?:,|and|&)\s*)?)+)/gi)) {
|
|
3368
3938
|
for (const n of m[1].matchAll(/#(\d+)/g)) if (open.has(Number(n[1]))) out.add(Number(n[1]));
|
|
3369
3939
|
}
|
|
3370
3940
|
return [...out];
|
|
3371
3941
|
}
|
|
3372
3942
|
var DEPENDENCY = /\b(?:depends\s+on|blocked\s+by)\s*:?\s*(?:#\d+(?:\s*(?:,|and|&)\s*)?)+/gi;
|
|
3373
3943
|
var STATEMENT = new RegExp(`\\s*${DEPENDENCY.source}[.;,]?`, "gi");
|
|
3374
|
-
function setDependsOn(
|
|
3944
|
+
function setDependsOn(body2, deps) {
|
|
3375
3945
|
const lines = [];
|
|
3376
3946
|
let dropped = false;
|
|
3377
|
-
for (const line of (
|
|
3947
|
+
for (const line of (body2 ?? "").split(/\r?\n/)) {
|
|
3378
3948
|
const rest2 = line.match(DEPENDENCY) ? line.replace(STATEMENT, "").trimEnd() : line;
|
|
3379
3949
|
if (rest2 !== line && !/[\p{L}\p{N}]/u.test(rest2)) {
|
|
3380
3950
|
dropped = true;
|
|
@@ -3423,7 +3993,7 @@ function holdUps(issues) {
|
|
|
3423
3993
|
}
|
|
3424
3994
|
|
|
3425
3995
|
// server/ceo.ts
|
|
3426
|
-
var EFFORT =
|
|
3996
|
+
var EFFORT = z2.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
3427
3997
|
function createOfficeTools(h) {
|
|
3428
3998
|
const run2 = async (fn) => {
|
|
3429
3999
|
try {
|
|
@@ -3442,21 +4012,21 @@ function createOfficeTools(h) {
|
|
|
3442
4012
|
tool(
|
|
3443
4013
|
"agent_detail",
|
|
3444
4014
|
"One agent in full: title, specialty, role, status, current task, model and effort, and their complete job description (company_status shortens long ones). Read it before rewriting someone's job description.",
|
|
3445
|
-
{ agent_id:
|
|
4015
|
+
{ agent_id: z2.string().describe("An id (or name) from company_status") },
|
|
3446
4016
|
(a) => run2(() => h.agentDetail(a))
|
|
3447
4017
|
),
|
|
3448
4018
|
tool(
|
|
3449
4019
|
"set_floor_profile",
|
|
3450
4020
|
"Record your read of a floor's project: a one-line summary (kind of project and stack), the QA brief that tells QA testers what to check for this kind of project, and how to run the app for the floor's preview monitor.",
|
|
3451
4021
|
{
|
|
3452
|
-
floor:
|
|
3453
|
-
summary:
|
|
3454
|
-
qa_brief:
|
|
3455
|
-
preview_command:
|
|
4022
|
+
floor: z2.number().int().describe("Floor number"),
|
|
4023
|
+
summary: z2.string().max(140).optional().describe('e.g. "3D browser game \xB7 Three.js + Vite + TypeScript"'),
|
|
4024
|
+
qa_brief: z2.string().max(2500).optional().describe("What QA must check on every PR for this project, as short bullet points"),
|
|
4025
|
+
preview_command: z2.string().max(2e3).optional().describe(
|
|
3456
4026
|
'Shell command that serves the app on port {port} from a fresh checkout after npm install, e.g. "npm run dev -- --port {port} --strictPort". PORT={port} is always set. {tmp} is a scratch folder. Empty string: back to the default (npm run dev, else start, else preview). Only set it when the default would not serve the app on PORT.'
|
|
3457
4027
|
),
|
|
3458
4028
|
// Not z.record(): the SDK can't turn it into JSON Schema, and one bad tool empties the whole tools/list.
|
|
3459
|
-
preview_env:
|
|
4029
|
+
preview_env: z2.object({}).catchall(z2.string()).optional().describe("Extra environment variables for the preview; {port} and {tmp} are replaced in the values. Replaces the whole set.")
|
|
3460
4030
|
},
|
|
3461
4031
|
(a) => run2(() => h.setFloorProfile(a))
|
|
3462
4032
|
),
|
|
@@ -3464,10 +4034,10 @@ function createOfficeTools(h) {
|
|
|
3464
4034
|
"update_job",
|
|
3465
4035
|
"Change an existing agent's job title, specialty or job description so it fits the project. Takes effect from their next task.",
|
|
3466
4036
|
{
|
|
3467
|
-
agent_id:
|
|
3468
|
-
title:
|
|
3469
|
-
specialty:
|
|
3470
|
-
job_description:
|
|
4037
|
+
agent_id: z2.string(),
|
|
4038
|
+
title: z2.string().max(60).optional(),
|
|
4039
|
+
specialty: z2.string().max(24).optional().describe('Short lowercase slug, e.g. "graphics"; "" for a generalist'),
|
|
4040
|
+
job_description: z2.string().max(2500).optional()
|
|
3471
4041
|
},
|
|
3472
4042
|
(a) => run2(() => h.updateJob(a))
|
|
3473
4043
|
),
|
|
@@ -3475,13 +4045,13 @@ function createOfficeTools(h) {
|
|
|
3475
4045
|
"propose_hire",
|
|
3476
4046
|
"Propose hiring a developer or QA tester for a floor. The manager approves or declines (or it is auto-approved if hiring is set to auto and the floor is under its team cap).",
|
|
3477
4047
|
{
|
|
3478
|
-
floor:
|
|
3479
|
-
role:
|
|
3480
|
-
title:
|
|
3481
|
-
specialty:
|
|
3482
|
-
job_description:
|
|
3483
|
-
reason:
|
|
3484
|
-
model:
|
|
4048
|
+
floor: z2.number().int(),
|
|
4049
|
+
role: z2.enum(["dev", "qa"]).describe("dev = builds issues into pull requests; qa = tests pull requests"),
|
|
4050
|
+
title: z2.string().max(60).describe('Specific job title, e.g. "Three.js graphics engineer"'),
|
|
4051
|
+
specialty: z2.string().max(24).describe('Short lowercase slug used to label issues swarm:<specialty>, e.g. "graphics"'),
|
|
4052
|
+
job_description: z2.string().max(2500).describe("What this person owns on this project and how they should work. Written to them, second person."),
|
|
4053
|
+
reason: z2.string().max(600).describe("Why the floor needs them now. The manager reads this."),
|
|
4054
|
+
model: z2.string().optional().describe("Leave out to use the default model"),
|
|
3485
4055
|
effort: EFFORT.optional().describe("Leave out to use the default effort")
|
|
3486
4056
|
},
|
|
3487
4057
|
(a) => run2(() => h.proposeHire(a))
|
|
@@ -3489,17 +4059,17 @@ function createOfficeTools(h) {
|
|
|
3489
4059
|
tool(
|
|
3490
4060
|
"propose_let_go",
|
|
3491
4061
|
"Propose letting an agent go (overstaffed floor, specialty no longer needed). The manager decides.",
|
|
3492
|
-
{ agent_id:
|
|
4062
|
+
{ agent_id: z2.string(), reason: z2.string().max(600) },
|
|
3493
4063
|
(a) => run2(() => h.proposeLetGo(a))
|
|
3494
4064
|
),
|
|
3495
4065
|
tool(
|
|
3496
4066
|
"file_issue",
|
|
3497
4067
|
'File a GitHub issue on a floor. A specialty routes it to that specialist first; when none is free, any free developer takes it. Write "Depends on #N" in the body only when it cannot start until #N is merged: the office will not start it until #N is closed.',
|
|
3498
4068
|
{
|
|
3499
|
-
floor:
|
|
3500
|
-
title:
|
|
3501
|
-
body:
|
|
3502
|
-
specialty:
|
|
4069
|
+
floor: z2.number().int(),
|
|
4070
|
+
title: z2.string().max(120),
|
|
4071
|
+
body: z2.string().max(6e3).describe("Context, what to build, acceptance criteria"),
|
|
4072
|
+
specialty: z2.string().max(24).optional()
|
|
3503
4073
|
},
|
|
3504
4074
|
(a) => run2(() => h.fileIssue(a))
|
|
3505
4075
|
),
|
|
@@ -3507,10 +4077,10 @@ function createOfficeTools(h) {
|
|
|
3507
4077
|
"route_issue",
|
|
3508
4078
|
'Fix the routing of an open issue instead of filing a duplicate: change its specialty, rewrite its "Depends on #N" line, or both. The rest of the body stays as it is.',
|
|
3509
4079
|
{
|
|
3510
|
-
floor:
|
|
3511
|
-
number:
|
|
3512
|
-
specialty:
|
|
3513
|
-
depends_on:
|
|
4080
|
+
floor: z2.number().int(),
|
|
4081
|
+
number: z2.number().int().positive().describe("The issue number"),
|
|
4082
|
+
specialty: z2.string().max(24).optional().describe('Sets swarm:<specialty> and removes any other; "" for none. Someone on the floor, or a pending proposal, must have it.'),
|
|
4083
|
+
depends_on: z2.array(z2.number().int().positive()).max(10).optional().describe("Issues it waits for; [] for none. Not for an issue in progress.")
|
|
3514
4084
|
},
|
|
3515
4085
|
(a) => run2(() => h.routeIssue(a))
|
|
3516
4086
|
)
|
|
@@ -3671,14 +4241,14 @@ function planRoute(r) {
|
|
|
3671
4241
|
if (d === r.number) throw new Error(`#${r.number} can't depend on itself.`);
|
|
3672
4242
|
if (!open.has(d)) throw new Error(r.closed(d) ? `#${d} is closed, so there's nothing to wait for.` : `There is no open issue #${d} on floor ${r.floor}.`);
|
|
3673
4243
|
}
|
|
3674
|
-
const
|
|
3675
|
-
const after = r.issues.map((i) => i.number === r.number ? { ...i, body } : i);
|
|
4244
|
+
const body2 = setDependsOn(issue2.body, deps);
|
|
4245
|
+
const after = r.issues.map((i) => i.number === r.number ? { ...i, body: body2 } : i);
|
|
3676
4246
|
const waits = new Map(after.map((i) => [i.number, blockers(i.body, open)]));
|
|
3677
4247
|
const loop = deps.find((d) => reaches(d, r.number, waits));
|
|
3678
4248
|
if (loop !== void 0) throw new Error(`#${loop} already waits for #${r.number}, directly or through other issues, so that would be a cycle.`);
|
|
3679
4249
|
const depth = waitsDepth(r.number, waits) + (holdUps(after).get(r.number)?.chain ?? 0);
|
|
3680
4250
|
if (depth > 2) throw new Error(`That makes a dependency chain ${depth} steps deep through #${r.number}. Keep chains to 2 steps at most: split the work so more of it can start side by side.`);
|
|
3681
|
-
if (
|
|
4251
|
+
if (body2 !== issue2.body) plan.body = body2;
|
|
3682
4252
|
done.push(deps.length ? `depends on ${deps.map((d) => `#${d}`).join(", ")}` : "no dependencies");
|
|
3683
4253
|
}
|
|
3684
4254
|
plan.summary = `#${r.number} on floor ${r.floor}: ${done.join(", ")}.`;
|
|
@@ -3690,8 +4260,8 @@ function reaches(from, to, waits, seen = /* @__PURE__ */ new Set()) {
|
|
|
3690
4260
|
seen.add(from);
|
|
3691
4261
|
return (waits.get(from) ?? []).some((n) => reaches(n, to, waits, seen));
|
|
3692
4262
|
}
|
|
3693
|
-
function specialtySlug(
|
|
3694
|
-
const slug = String(
|
|
4263
|
+
function specialtySlug(s2) {
|
|
4264
|
+
const slug = String(s2 ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
3695
4265
|
return slug === "skip" ? "" : slug;
|
|
3696
4266
|
}
|
|
3697
4267
|
var specialtyLabel = (slug) => `swarm:${slug}`;
|
|
@@ -3723,15 +4293,6 @@ function fixOutcome(rec, head, minutes, counts = true) {
|
|
|
3723
4293
|
return { set: { status: "queued", round: rec.round + 1, sessionFailures: 0 }, log: { kind: "done", text: `\u2714 Fix pushed for PR #${pr} in ${minutes}m. Back to QA.` }, pushed: true };
|
|
3724
4294
|
}
|
|
3725
4295
|
|
|
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
4296
|
// server/issueClaims.ts
|
|
3736
4297
|
var RECENT_MERGE_MS = 6 * 60 * 6e4;
|
|
3737
4298
|
function branchIssue(headRefName) {
|
|
@@ -3751,38 +4312,6 @@ function issueTaken(n, agents, pulls, now2) {
|
|
|
3751
4312
|
});
|
|
3752
4313
|
}
|
|
3753
4314
|
|
|
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
4315
|
// server/qaOrphans.ts
|
|
3787
4316
|
function orphanedQa(qa, agents, busy) {
|
|
3788
4317
|
return qa.filter(
|
|
@@ -3790,24 +4319,6 @@ function orphanedQa(qa, agents, busy) {
|
|
|
3790
4319
|
);
|
|
3791
4320
|
}
|
|
3792
4321
|
|
|
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
4322
|
// server/qaPrompt.ts
|
|
3812
4323
|
function qaInstructions(i) {
|
|
3813
4324
|
return { checks: checksLine(i), testStep: testStep(i.checks !== "none"), retest: retest(i) };
|
|
@@ -3970,9 +4481,9 @@ var Previews = class {
|
|
|
3970
4481
|
r.handle = this.backend.previews.start(
|
|
3971
4482
|
{ fullName: f.fullName, defaultBranch: f.defaultBranch, pr: pr ?? null, port, command: f.preview.command, env: { ...f.preview.env }, title: `${title} \xB7 ${ref}` },
|
|
3972
4483
|
{
|
|
3973
|
-
status: (
|
|
4484
|
+
status: (s2) => {
|
|
3974
4485
|
if (!live()) return;
|
|
3975
|
-
r.status =
|
|
4486
|
+
r.status = s2;
|
|
3976
4487
|
this.hooks.emit(f.id);
|
|
3977
4488
|
},
|
|
3978
4489
|
commit: (sha) => {
|
|
@@ -4074,7 +4585,7 @@ var PLACEHOLDERS = {
|
|
|
4074
4585
|
};
|
|
4075
4586
|
var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
|
|
4076
4587
|
var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
|
|
4077
|
-
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing }) {
|
|
4588
|
+
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing, depsLine }) {
|
|
4078
4589
|
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
4079
4590
|
const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
|
|
4080
4591
|
return [
|
|
@@ -4088,6 +4599,7 @@ ${a.brief}` : "",
|
|
|
4088
4599
|
repo.summary ? `Project: ${repo.summary}` : "",
|
|
4089
4600
|
repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
|
|
4090
4601
|
`Your worktree: ${cwd}`,
|
|
4602
|
+
depsLine ?? "",
|
|
4091
4603
|
fixing ? `You are fixing pull request #${fixing.pr}. Its code is checked out on local branch ${branch}; push fixes with: ${push}. Do not open a new pull request.` : `Your branch: ${branch} (already checked out, created from origin/${repo.defaultBranch})`,
|
|
4092
4604
|
links.length ? `Related repositories you may read for context (do not modify them):
|
|
4093
4605
|
${links.join("\n")}` : "",
|
|
@@ -4104,7 +4616,7 @@ ${links.join("\n")}` : "",
|
|
|
4104
4616
|
"Rules: never push to the default branch, never force-push, never merge pull requests yourself (the office merges them once QA and the checks pass), and never edit files outside your worktree. If you cannot finish, open a draft PR (gh pr create --draft) explaining what is left and why."
|
|
4105
4617
|
].filter((l) => l !== "").join("\n");
|
|
4106
4618
|
}
|
|
4107
|
-
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, testStep: testStep2 }) {
|
|
4619
|
+
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, depsLine, testStep: testStep2 }) {
|
|
4108
4620
|
return [
|
|
4109
4621
|
`You are ${a.name}, ${a.title ? `the team's ${a.title},` : "a QA engineer"} on an autonomous agent team ("cubefarm"). Developers open pull requests; you review and independently verify each one before it is merged. Your sign-off is the review: ${repo.autoMerge ? "on this floor a PR you pass merges by itself as soon as GitHub's checks are green, so nobody else reads the code after you. " : ""}Be thorough and skeptical, but fair: fail a PR only for real problems (broken behaviour, failing tests or build, the issue's requirements not met, obvious regressions), not for style preferences.`,
|
|
4110
4622
|
...a.brief ? ["", `Your job description:
|
|
@@ -4118,6 +4630,7 @@ ${a.brief}`] : [],
|
|
|
4118
4630
|
${repo.qaBrief}`] : [],
|
|
4119
4631
|
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
4120
4632
|
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
4633
|
+
...depsLine ? [depsLine] : [],
|
|
4121
4634
|
"",
|
|
4122
4635
|
"How to test:",
|
|
4123
4636
|
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
@@ -4191,8 +4704,8 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
4191
4704
|
}
|
|
4192
4705
|
|
|
4193
4706
|
// server/terminal.ts
|
|
4194
|
-
import
|
|
4195
|
-
import
|
|
4707
|
+
import fs12 from "node:fs/promises";
|
|
4708
|
+
import path14 from "node:path";
|
|
4196
4709
|
import xtermHeadless from "@xterm/headless";
|
|
4197
4710
|
import xtermSerialize from "@xterm/addon-serialize";
|
|
4198
4711
|
var { Terminal } = xtermHeadless;
|
|
@@ -4289,12 +4802,12 @@ var AgentTerminal = class {
|
|
|
4289
4802
|
async save(file) {
|
|
4290
4803
|
this.dirty = false;
|
|
4291
4804
|
await this.flush();
|
|
4292
|
-
await
|
|
4293
|
-
await
|
|
4294
|
-
await
|
|
4805
|
+
await fs12.mkdir(path14.dirname(file), { recursive: true });
|
|
4806
|
+
await fs12.writeFile(`${file}.tmp`, this.snapshot());
|
|
4807
|
+
await fs12.rename(`${file}.tmp`, file);
|
|
4295
4808
|
}
|
|
4296
4809
|
async load(file) {
|
|
4297
|
-
const data = await
|
|
4810
|
+
const data = await fs12.readFile(file, "utf8").catch(() => "");
|
|
4298
4811
|
if (!data) return;
|
|
4299
4812
|
this.term.write(data);
|
|
4300
4813
|
this.note("(the office restarted)");
|
|
@@ -4386,6 +4899,7 @@ function effectiveModel(own, cli, office, claudeDefault) {
|
|
|
4386
4899
|
|
|
4387
4900
|
// shared/types.ts
|
|
4388
4901
|
var CEO_ID = "ceo";
|
|
4902
|
+
var INSTALL_STEP = "Installing dependencies";
|
|
4389
4903
|
|
|
4390
4904
|
// server/swarm.ts
|
|
4391
4905
|
var FLOOR_COLORS = ["#ff8a5b", "#4fb3e8", "#8fd14f", "#c77dff", "#ffc93c", "#ff6fb5", "#2ec4b6", "#f25f5c"];
|
|
@@ -4430,27 +4944,27 @@ var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
|
4430
4944
|
var CEO_MODEL = "claude-opus-5-5";
|
|
4431
4945
|
var CEO_EFFORT = "xhigh";
|
|
4432
4946
|
var CEO_NAME = "Morgan";
|
|
4433
|
-
var CEO_DIR =
|
|
4434
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
4947
|
+
var CEO_DIR = path15.join(HOME_DIR, "ceo");
|
|
4948
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path15.resolve(import.meta.dirname, ".."));
|
|
4435
4949
|
var MAX_ISSUES_PER_JOB = 12;
|
|
4436
4950
|
var KEEP_MESSAGES = 200;
|
|
4437
4951
|
var KEEP_DECIDED_REQUESTS = 40;
|
|
4438
4952
|
var pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
4439
|
-
var slugify = (
|
|
4953
|
+
var slugify = (s2) => s2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "agent";
|
|
4440
4954
|
var BUSY = ["preparing", "working"];
|
|
4441
4955
|
var FREE = ["idle", "done"];
|
|
4442
4956
|
var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
4443
4957
|
var MAX_ISSUE_FAILURES = 2;
|
|
4444
4958
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
4445
4959
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
4446
|
-
var SCREENS_DIR =
|
|
4960
|
+
var SCREENS_DIR = path15.join(HOME_DIR, "screens");
|
|
4447
4961
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
4448
|
-
var screenFile = (agentId, mime) =>
|
|
4962
|
+
var screenFile = (agentId, mime) => path15.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
4449
4963
|
async function loadScreen(agentId) {
|
|
4450
4964
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
4451
|
-
const file =
|
|
4965
|
+
const file = path15.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
4452
4966
|
try {
|
|
4453
|
-
const [data, stat] = await Promise.all([
|
|
4967
|
+
const [data, stat] = await Promise.all([fs13.readFile(file), fs13.stat(file)]);
|
|
4454
4968
|
return { data, mime, at: stat.mtimeMs };
|
|
4455
4969
|
} catch {
|
|
4456
4970
|
}
|
|
@@ -4458,10 +4972,10 @@ async function loadScreen(agentId) {
|
|
|
4458
4972
|
return null;
|
|
4459
4973
|
}
|
|
4460
4974
|
async function removeScreens(agentId) {
|
|
4461
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
4975
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs13.rm(path15.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
4462
4976
|
}
|
|
4463
|
-
var TERMINALS_DIR =
|
|
4464
|
-
var terminalFile = (agentId) =>
|
|
4977
|
+
var TERMINALS_DIR = path15.join(HOME_DIR, "terminals");
|
|
4978
|
+
var terminalFile = (agentId) => path15.join(TERMINALS_DIR, `${agentId}.ansi`);
|
|
4465
4979
|
var TERMINAL_SAVE_MS = 2e4;
|
|
4466
4980
|
var QA_SCHEMA = {
|
|
4467
4981
|
type: "object",
|
|
@@ -4527,12 +5041,20 @@ function parseReport(result) {
|
|
|
4527
5041
|
fixInstructions: r.fixInstructions ? String(r.fixInstructions) : void 0
|
|
4528
5042
|
};
|
|
4529
5043
|
}
|
|
4530
|
-
var cell = (
|
|
5044
|
+
var cell = (s2) => s2.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
4531
5045
|
var ICON = { pass: "\u2705", fail: "\u274C", skip: "\u23ED\uFE0F" };
|
|
4532
5046
|
var Swarm = class {
|
|
4533
|
-
// coding-agent CLIs found on this machine (detected at startup)
|
|
4534
5047
|
constructor(backend) {
|
|
4535
5048
|
this.backend = backend;
|
|
5049
|
+
this.voice = new Voice({
|
|
5050
|
+
api: backend.voice,
|
|
5051
|
+
secretsFile: path15.join(HOME_DIR, backend.demo ? "demo-secrets.json" : "secrets.json"),
|
|
5052
|
+
cacheDir: path15.join(HOME_DIR, backend.demo ? "demo-voice" : "voice"),
|
|
5053
|
+
settings: () => this.state.settings.voice,
|
|
5054
|
+
message: (id) => this.state.messages.find((m) => m.id === id),
|
|
5055
|
+
officeNote: (text) => this.postMessage("office", text),
|
|
5056
|
+
keyChanged: (view2) => this.broadcast({ type: "voiceKey", ...view2 })
|
|
5057
|
+
});
|
|
4536
5058
|
this.previews = new Previews(backend, {
|
|
4537
5059
|
emit: (id) => {
|
|
4538
5060
|
const r = this.state.repos.find((x) => x.id === id);
|
|
@@ -4558,7 +5080,8 @@ var Swarm = class {
|
|
|
4558
5080
|
setupDone: false,
|
|
4559
5081
|
tutorialStep: 0,
|
|
4560
5082
|
autoUpdate: true,
|
|
4561
|
-
pacingSessions: DEFAULT_PACING_SESSIONS
|
|
5083
|
+
pacingSessions: DEFAULT_PACING_SESSIONS,
|
|
5084
|
+
voice: { ...DEFAULT_VOICE }
|
|
4562
5085
|
},
|
|
4563
5086
|
repos: [],
|
|
4564
5087
|
agents: [],
|
|
@@ -4614,10 +5137,13 @@ var Swarm = class {
|
|
|
4614
5137
|
};
|
|
4615
5138
|
lastOfficeView = "";
|
|
4616
5139
|
clis = [];
|
|
5140
|
+
// coding-agent CLIs found on this machine (detected at startup)
|
|
5141
|
+
/** Phone messages read aloud. The demo keeps its own key and clips, so it never touches the real ones. */
|
|
5142
|
+
voice;
|
|
4617
5143
|
// ---------- lifecycle ----------
|
|
4618
5144
|
async init() {
|
|
4619
5145
|
try {
|
|
4620
|
-
const raw = await
|
|
5146
|
+
const raw = await fs13.readFile(STATE_FILE, "utf8");
|
|
4621
5147
|
const loaded = JSON.parse(raw);
|
|
4622
5148
|
this.state = {
|
|
4623
5149
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -4672,11 +5198,13 @@ var Swarm = class {
|
|
|
4672
5198
|
delete old.maxConcurrent;
|
|
4673
5199
|
}
|
|
4674
5200
|
delete old.permissionMode;
|
|
5201
|
+
this.state.settings.voice = voiceSettings(DEFAULT_VOICE, loaded.settings?.voice);
|
|
4675
5202
|
if (loaded.settings && loaded.settings.setupDone === void 0 && this.state.repos.length > 0) {
|
|
4676
5203
|
Object.assign(this.state.settings, { setupDone: true, tutorialStep: -1 });
|
|
4677
5204
|
}
|
|
4678
5205
|
} catch {
|
|
4679
5206
|
}
|
|
5207
|
+
await this.voice.init();
|
|
4680
5208
|
for (const r of this.state.repos) if (r.localPath) this.backend.setLocalPath(r.fullName, r.localPath);
|
|
4681
5209
|
const interrupted = [];
|
|
4682
5210
|
for (const a of this.state.agents) {
|
|
@@ -4872,12 +5400,15 @@ var Swarm = class {
|
|
|
4872
5400
|
phoneReadAt: this.state.phoneReadAt,
|
|
4873
5401
|
usage: this.usageNow(),
|
|
4874
5402
|
clis: this.clis,
|
|
5403
|
+
...this.voice.keyView(),
|
|
4875
5404
|
officeCommit: this.officeHead?.slice(0, 7) ?? null,
|
|
4876
5405
|
officeUpdate: this.officeHead ? this.officeUpdateView() : void 0
|
|
4877
5406
|
};
|
|
4878
5407
|
}
|
|
5408
|
+
/** The agent's latest screenshot: null when it has none right now, undefined for an unknown agent. */
|
|
4879
5409
|
screenshot(agentId) {
|
|
4880
|
-
|
|
5410
|
+
const rt = this.agentRt.get(agentId);
|
|
5411
|
+
return rt ? rt.screenshot : void 0;
|
|
4881
5412
|
}
|
|
4882
5413
|
// ---------- clients ----------
|
|
4883
5414
|
addClient(ws) {
|
|
@@ -4939,10 +5470,10 @@ var Swarm = class {
|
|
|
4939
5470
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
4940
5471
|
this.saveTimer = null;
|
|
4941
5472
|
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
4942
|
-
await
|
|
5473
|
+
await fs13.mkdir(path15.dirname(STATE_FILE), { recursive: true });
|
|
4943
5474
|
const tmp = `${STATE_FILE}.tmp`;
|
|
4944
|
-
await
|
|
4945
|
-
await
|
|
5475
|
+
await fs13.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
5476
|
+
await fs13.rename(tmp, STATE_FILE);
|
|
4946
5477
|
}
|
|
4947
5478
|
// ---------- lookups ----------
|
|
4948
5479
|
repo(id) {
|
|
@@ -5024,7 +5555,7 @@ var Swarm = class {
|
|
|
5024
5555
|
const name = fullName.split("/")[1];
|
|
5025
5556
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
5026
5557
|
if (taken) throw new HttpError(409, `${taken.path} already exists and isn't a clone of ${fullName}. Connect that folder instead, or rename it first.`);
|
|
5027
|
-
return
|
|
5558
|
+
return path15.join(root, name);
|
|
5028
5559
|
}
|
|
5029
5560
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
5030
5561
|
async createRepo(name, opts) {
|
|
@@ -5036,11 +5567,11 @@ var Swarm = class {
|
|
|
5036
5567
|
}
|
|
5037
5568
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
5038
5569
|
async listProjectFolders(dir) {
|
|
5039
|
-
const root = dir?.trim() ?
|
|
5570
|
+
const root = dir?.trim() ? path15.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
5040
5571
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
5041
5572
|
throw new HttpError(400, err.message);
|
|
5042
5573
|
});
|
|
5043
|
-
const same = (a, b) =>
|
|
5574
|
+
const same = (a, b) => path15.resolve(a).toLowerCase() === path15.resolve(b).toLowerCase();
|
|
5044
5575
|
const floorOf = (f) => this.state.repos.find((r) => r.localPath && same(r.localPath, f.path) || f.github && r.id.toLowerCase() === f.github.toLowerCase())?.floor ?? null;
|
|
5045
5576
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
5046
5577
|
}
|
|
@@ -5218,13 +5749,13 @@ var Swarm = class {
|
|
|
5218
5749
|
return false;
|
|
5219
5750
|
}
|
|
5220
5751
|
/** 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,
|
|
5752
|
+
sendBack(repo, rec, reason2, fixInstructions, needsHuman) {
|
|
5222
5753
|
if (needsHuman) {
|
|
5223
5754
|
this.setQa(rec, { status: "needs-human", mergeNote: null });
|
|
5224
|
-
this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} still ${
|
|
5755
|
+
this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} still ${reason2 === "conflict" ? `conflicts with ${repo.defaultBranch}` : "fails its checks"} after ${MAX_MERGE_FIXES} fixes, so it needs you.`);
|
|
5225
5756
|
return false;
|
|
5226
5757
|
}
|
|
5227
|
-
this.setQa(rec, { status: "failed", fixReason:
|
|
5758
|
+
this.setQa(rec, { status: "failed", fixReason: reason2, fixInstructions, mergeFixes: rec.mergeFixes + 1, mergeNote: null, pendingSince: null });
|
|
5228
5759
|
setTimeout(() => this.schedule(), 200);
|
|
5229
5760
|
return false;
|
|
5230
5761
|
}
|
|
@@ -5241,7 +5772,7 @@ var Swarm = class {
|
|
|
5241
5772
|
rt.folderSync = status;
|
|
5242
5773
|
this.emitRepo(repo);
|
|
5243
5774
|
if (status && status !== before) {
|
|
5244
|
-
const name = repo.localPath ?
|
|
5775
|
+
const name = repo.localPath ? path15.basename(repo.localPath) : repo.fullName;
|
|
5245
5776
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
5246
5777
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
5247
5778
|
}
|
|
@@ -5265,7 +5796,7 @@ var Swarm = class {
|
|
|
5265
5796
|
};
|
|
5266
5797
|
const r = await this.backend.sweepDesks(repo.fullName, keep);
|
|
5267
5798
|
const desks = r.desks + r.folders;
|
|
5268
|
-
const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${
|
|
5799
|
+
const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${path15.dirname(r.patches[0])})` : "";
|
|
5269
5800
|
console.log(`desk sweep ${repo.fullName}: ${desks} desks, ${r.branches} branches removed${saved}${r.skipped.length ? `, ${r.skipped.length} still in use` : ""}`);
|
|
5270
5801
|
if (!desks && !r.branches) return;
|
|
5271
5802
|
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 +5900,11 @@ var Swarm = class {
|
|
|
5369
5900
|
}
|
|
5370
5901
|
this.save();
|
|
5371
5902
|
}
|
|
5372
|
-
async createIssue(repoId2, title,
|
|
5903
|
+
async createIssue(repoId2, title, body2, assignTo, specialty) {
|
|
5373
5904
|
const repo = this.repo(repoId2);
|
|
5374
5905
|
if (!title.trim()) throw new HttpError(400, "An issue needs a title");
|
|
5375
5906
|
const slug = specialtySlug(specialty);
|
|
5376
|
-
const number = await this.backend.createIssue(repo.fullName, title.trim(),
|
|
5907
|
+
const number = await this.backend.createIssue(repo.fullName, title.trim(), body2, slug ? [specialtyLabel(slug)] : []);
|
|
5377
5908
|
await this.syncRepo(repo.id);
|
|
5378
5909
|
this.toast("success", `Issue #${number} filed on ${repo.fullName}`);
|
|
5379
5910
|
if (assignTo) await this.assign(assignTo, number);
|
|
@@ -5406,7 +5937,7 @@ var Swarm = class {
|
|
|
5406
5937
|
}
|
|
5407
5938
|
const name = opts.name?.trim() || this.freeName(role);
|
|
5408
5939
|
const agent = {
|
|
5409
|
-
id:
|
|
5940
|
+
id: crypto7.randomUUID(),
|
|
5410
5941
|
name,
|
|
5411
5942
|
repoId: repo.id,
|
|
5412
5943
|
role,
|
|
@@ -5493,7 +6024,7 @@ var Swarm = class {
|
|
|
5493
6024
|
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
5494
6025
|
this.agentRt.get(id)?.terminal?.dispose();
|
|
5495
6026
|
void removeScreens(id);
|
|
5496
|
-
void
|
|
6027
|
+
void fs13.rm(terminalFile(id), { force: true }).catch(() => void 0);
|
|
5497
6028
|
for (const q of this.state.qa) {
|
|
5498
6029
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
5499
6030
|
if (q.devAgentId === id) q.devAgentId = null;
|
|
@@ -5600,7 +6131,7 @@ var Swarm = class {
|
|
|
5600
6131
|
}
|
|
5601
6132
|
// ---------- terminals ----------
|
|
5602
6133
|
async loadTerminal(agentId) {
|
|
5603
|
-
if (!await
|
|
6134
|
+
if (!await fs13.stat(terminalFile(agentId)).catch(() => null)) return null;
|
|
5604
6135
|
const t = this.newTerminal(agentId);
|
|
5605
6136
|
await t.load(terminalFile(agentId));
|
|
5606
6137
|
return t;
|
|
@@ -5630,8 +6161,8 @@ var Swarm = class {
|
|
|
5630
6161
|
if (!t) return ws.close(4404, "That agent has no terminal");
|
|
5631
6162
|
t.attach(ws);
|
|
5632
6163
|
}
|
|
5633
|
-
buildSystemAppend(a, repo, cwd, branch, fixing) {
|
|
5634
|
-
return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing });
|
|
6164
|
+
buildSystemAppend(a, repo, cwd, branch, fixing, deps) {
|
|
6165
|
+
return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing, depsLine: depsPromptLine(deps) });
|
|
5635
6166
|
}
|
|
5636
6167
|
linkedDirs(repo) {
|
|
5637
6168
|
return this.linkedRepos(repo).map((r) => ({ fullName: r.fullName, dir: this.backend.mainDir(r.fullName) }));
|
|
@@ -5677,7 +6208,9 @@ var Swarm = class {
|
|
|
5677
6208
|
prepare: () => this.backend.prepareDesk(repo.fullName, { defaultBranch: repo.defaultBranch, pr: base.pr }, slug, branch, note)
|
|
5678
6209
|
});
|
|
5679
6210
|
this.deskAlerts.delete(a.id);
|
|
5680
|
-
|
|
6211
|
+
if (a.status !== "preparing") return null;
|
|
6212
|
+
const deps = await this.installDeps(a, cwd);
|
|
6213
|
+
return a.status === "preparing" ? { cwd, deps } : null;
|
|
5681
6214
|
} catch (err) {
|
|
5682
6215
|
if (a.status !== "preparing") return null;
|
|
5683
6216
|
a.status = "error";
|
|
@@ -5693,6 +6226,25 @@ var Swarm = class {
|
|
|
5693
6226
|
return null;
|
|
5694
6227
|
}
|
|
5695
6228
|
}
|
|
6229
|
+
/** The desk's dependencies, shown on the agent's card ("Installing dependencies") while npm runs. */
|
|
6230
|
+
async installDeps(a, cwd) {
|
|
6231
|
+
const rt = this.agentRt.get(a.id);
|
|
6232
|
+
try {
|
|
6233
|
+
return await this.backend.installDeps(cwd, {
|
|
6234
|
+
log: (lines) => this.appendLog(a, lines.map((text) => ({ kind: text.startsWith("\u26A0") ? "error" : "system", text }))),
|
|
6235
|
+
installing: () => {
|
|
6236
|
+
if (!rt) return;
|
|
6237
|
+
rt.currentTool = INSTALL_STEP;
|
|
6238
|
+
this.emitAgent(a);
|
|
6239
|
+
}
|
|
6240
|
+
});
|
|
6241
|
+
} finally {
|
|
6242
|
+
if (rt?.currentTool === INSTALL_STEP) {
|
|
6243
|
+
rt.currentTool = null;
|
|
6244
|
+
this.emitAgent(a);
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
5696
6248
|
async runTask(a, repo, issue2, note) {
|
|
5697
6249
|
const branch = devBranch(issue2.number, slugify(a.name));
|
|
5698
6250
|
this.beginTask(
|
|
@@ -5701,8 +6253,9 @@ var Swarm = class {
|
|
|
5701
6253
|
`Issue #${issue2.number}: ${issue2.title}`,
|
|
5702
6254
|
`Preparing worktree on ${branch}\u2026`
|
|
5703
6255
|
);
|
|
5704
|
-
const
|
|
5705
|
-
if (!
|
|
6256
|
+
const desk = await this.prepare(a, repo, {}, branch);
|
|
6257
|
+
if (!desk) return;
|
|
6258
|
+
const { cwd } = desk;
|
|
5706
6259
|
const prompt = [
|
|
5707
6260
|
`Please resolve GitHub issue #${issue2.number}: ${issue2.title}`,
|
|
5708
6261
|
`URL: ${issue2.url}`,
|
|
@@ -5712,7 +6265,7 @@ var Swarm = class {
|
|
|
5712
6265
|
note ? `
|
|
5713
6266
|
Note from the manager: ${note}` : ""
|
|
5714
6267
|
].filter(Boolean).join("\n");
|
|
5715
|
-
this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch));
|
|
6268
|
+
this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch, void 0, desk.deps));
|
|
5716
6269
|
}
|
|
5717
6270
|
startAgentSession(a, repo, cwd, prompt, systemAppend, resumeSessionId, outputSchema, mode = null) {
|
|
5718
6271
|
const rt = this.agentRt.get(a.id);
|
|
@@ -5759,7 +6312,7 @@ Note from the manager: ${note}` : ""
|
|
|
5759
6312
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
5760
6313
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
5761
6314
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
5762
|
-
void removeScreens(a.id).then(() =>
|
|
6315
|
+
void removeScreens(a.id).then(() => fs13.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs13.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
|
|
5763
6316
|
},
|
|
5764
6317
|
limited: (at) => this.pauseForLimit(at),
|
|
5765
6318
|
usageWarning: (info) => this.paceForWarning(info),
|
|
@@ -5895,8 +6448,22 @@ Note from the manager: ${note}` : ""
|
|
|
5895
6448
|
this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
|
|
5896
6449
|
this.toast("info", `PR #${prNumber} is queued for QA`);
|
|
5897
6450
|
}
|
|
5898
|
-
|
|
5899
|
-
|
|
6451
|
+
/** Manager's "send back to dev" for a PR that needs a human or failed QA: a developer fixes it, with the note. */
|
|
6452
|
+
async sendBackToDev(repoId2, prNumber, note) {
|
|
6453
|
+
const repo = this.repo(repoId2);
|
|
6454
|
+
const find = () => this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === prNumber);
|
|
6455
|
+
const listed = this.repoRt.get(repo.id)?.pulls.find((p) => p.number === prNumber);
|
|
6456
|
+
sendBackPatch(prNumber, listed, find(), repo.defaultBranch, note);
|
|
6457
|
+
const details = await this.backend.prDetails(repo.fullName, prNumber).catch(() => null);
|
|
6458
|
+
const pr = details && listed ? { ...listed, state: details.state, mergeable: details.mergeable, mergeState: details.mergeState } : listed;
|
|
6459
|
+
const rec = find();
|
|
6460
|
+
const patch = sendBackPatch(prNumber, pr, rec, repo.defaultBranch, note);
|
|
6461
|
+
this.setQa(rec, patch);
|
|
6462
|
+
setTimeout(() => this.schedule(), 200);
|
|
6463
|
+
this.toast("info", `PR #${prNumber} goes back to a developer${patch.fixReason === "conflict" ? " to resolve its conflicts" : ""}`);
|
|
6464
|
+
}
|
|
6465
|
+
buildQaSystemAppend(a, repo, cwd, branch, pr, testStep2, deps) {
|
|
6466
|
+
return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr, testStep: testStep2, depsLine: depsPromptLine(deps) });
|
|
5900
6467
|
}
|
|
5901
6468
|
async runQa(a, repo, rec) {
|
|
5902
6469
|
const branch = qaBranch(rec.prNumber, slugify(a.name));
|
|
@@ -5933,11 +6500,12 @@ Note from the manager: ${note}` : ""
|
|
|
5933
6500
|
this.clearTask(a);
|
|
5934
6501
|
return;
|
|
5935
6502
|
}
|
|
5936
|
-
const
|
|
5937
|
-
if (!
|
|
6503
|
+
const desk = await this.prepare(a, repo, { pr: rec.prNumber }, branch);
|
|
6504
|
+
if (!desk) {
|
|
5938
6505
|
if (this.state.qa.includes(rec) && rec.status === "testing") this.setQa(rec, { status: "queued", qaAgentId: null });
|
|
5939
6506
|
return;
|
|
5940
6507
|
}
|
|
6508
|
+
const { cwd } = desk;
|
|
5941
6509
|
const dev = rec.devAgentId ? this.state.agents.find((x) => x.id === rec.devAgentId) : null;
|
|
5942
6510
|
const failedConflict = rec.fixReason === "conflict" && !rec.passedSha;
|
|
5943
6511
|
const qa = qaInstructions({
|
|
@@ -5962,7 +6530,7 @@ Note from the manager: ${note}` : ""
|
|
|
5962
6530
|
Linked issue: ${issue2.title}
|
|
5963
6531
|
${issue2.body.trim() || "(no description)"}` : ""
|
|
5964
6532
|
].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);
|
|
6533
|
+
this.startAgentSession(a, repo, cwd, prompt, this.buildQaSystemAppend(a, repo, cwd, branch, pr, qa.testStep, desk.deps), void 0, QA_SCHEMA);
|
|
5966
6534
|
}
|
|
5967
6535
|
async onQaFinished(a, repo, result) {
|
|
5968
6536
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
@@ -5990,8 +6558,8 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5990
6558
|
const next = qaOutcome(pass, rec, pull);
|
|
5991
6559
|
try {
|
|
5992
6560
|
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,
|
|
6561
|
+
const body2 = await this.renderQaComment(a, repo, rec, report, rt.shots, next);
|
|
6562
|
+
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber, body2) || null;
|
|
5995
6563
|
this.appendLog(a, [{ kind: "system", text: ` \u23BF ${commentUrl ?? "comment posted"}` }]);
|
|
5996
6564
|
} catch (err) {
|
|
5997
6565
|
this.appendLog(a, [{ kind: "error", text: ` \u23BF Could not post the QA report: ${err.message}` }]);
|
|
@@ -6075,16 +6643,20 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
6075
6643
|
rec.fixReason === "conflict" ? `Resolving conflicts on PR #${rec.prNumber}` : rec.fixReason === "checks" ? `Fixing checks on PR #${rec.prNumber}` : `Fixing PR #${rec.prNumber} after QA round ${rec.round}`,
|
|
6076
6644
|
`Checking out PR #${rec.prNumber}\u2026`
|
|
6077
6645
|
);
|
|
6078
|
-
const
|
|
6079
|
-
if (!
|
|
6646
|
+
const desk = await this.prepare(dev, repo, { pr: rec.prNumber }, headRef);
|
|
6647
|
+
if (!desk) {
|
|
6080
6648
|
if (rec.status === "fixing") this.setQa(rec, { status: "failed" });
|
|
6081
6649
|
return;
|
|
6082
6650
|
}
|
|
6651
|
+
const { cwd } = desk;
|
|
6083
6652
|
const failed2 = rec.checks.filter((c) => c.result === "fail");
|
|
6084
6653
|
const takeover = original ? "" : " A teammate wrote it, so read the PR and the linked issue first.";
|
|
6085
6654
|
const push = `push to the same branch: git push origin HEAD:${headRef}`;
|
|
6086
6655
|
const mergeFix = rec.fixReason === "conflict" && rec.passedSha ? [
|
|
6087
6656
|
`QA passed pull request #${rec.prNumber} (${pull?.url ?? ""}), but it now conflicts with ${repo.defaultBranch} because other work was merged first.${takeover}`,
|
|
6657
|
+
rec.fixInstructions ? `
|
|
6658
|
+
What needs fixing:
|
|
6659
|
+
${rec.fixInstructions}` : "",
|
|
6088
6660
|
"",
|
|
6089
6661
|
`Bring it up to date: git fetch origin && git merge origin/${repo.defaultBranch}. Resolve the conflicts so both this change and the newly merged work keep working, run the project's checks, and ${push}`
|
|
6090
6662
|
] : rec.fixReason === "checks" ? [
|
|
@@ -6114,7 +6686,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6114
6686
|
const mergeEnd = "Then reply with a short summary of what you did. Do not open a new pull request; the office merges it once the checks pass, after another QA round if the code changed.";
|
|
6115
6687
|
const prompt = (mergeFix ? [...mergeFix, "", mergeEnd] : qaFix).filter((l) => l !== "").join("\n");
|
|
6116
6688
|
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);
|
|
6689
|
+
this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }, desk.deps), resume);
|
|
6118
6690
|
}
|
|
6119
6691
|
async onFixFinished(a, repo, result) {
|
|
6120
6692
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
@@ -6190,30 +6762,31 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6190
6762
|
this.startAgentSession(a, repo, cwd, text, this.buildSystemAppend(a, repo, cwd, a.branch, fixing), a.sessionId, void 0, typed ? "typed" : null);
|
|
6191
6763
|
}
|
|
6192
6764
|
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)
|
|
6765
|
+
const s2 = this.state.settings;
|
|
6766
|
+
if (patch.sessionLimit !== void 0) s2.sessionLimit = Math.max(0, Math.round(Number(patch.sessionLimit)) || 0);
|
|
6767
|
+
if (isCli(patch.defaultCli) && patch.defaultCli !== s2.defaultCli) {
|
|
6768
|
+
s2.defaultCli = patch.defaultCli;
|
|
6769
|
+
if (patch.defaultModel === void 0) s2.defaultModel = s2.defaultCli === "claude" ? DEFAULT_MODEL : "";
|
|
6770
|
+
}
|
|
6771
|
+
if (patch.defaultModel !== void 0) s2.defaultModel = String(patch.defaultModel).trim() || (s2.defaultCli === "claude" ? DEFAULT_MODEL : "");
|
|
6772
|
+
if (patch.defaultEffort !== void 0 && EFFORTS.includes(patch.defaultEffort)) s2.defaultEffort = patch.defaultEffort;
|
|
6773
|
+
if (patch.runtime === "terminal" || patch.runtime === "sdk") s2.runtime = patch.runtime;
|
|
6774
|
+
if (patch.hiring === "approve" || patch.hiring === "auto") s2.hiring = patch.hiring;
|
|
6775
|
+
if (patch.teamCap !== void 0) s2.teamCap = Math.max(1, Math.min(15, Math.round(Number(patch.teamCap)) || 1));
|
|
6776
|
+
if (patch.ceoHeartbeatMin !== void 0) s2.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
6777
|
+
if (typeof patch.managerName === "string") s2.managerName = patch.managerName.trim().slice(0, 40);
|
|
6778
|
+
if (typeof patch.companyName === "string") s2.companyName = patch.companyName.trim().slice(0, 60);
|
|
6779
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s2.projectsDir = path15.resolve(patch.projectsDir.trim());
|
|
6780
|
+
if (typeof patch.setupDone === "boolean") s2.setupDone = patch.setupDone;
|
|
6781
|
+
if (patch.tutorialStep !== void 0) s2.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
6782
|
+
if (typeof patch.autoUpdate === "boolean") s2.autoUpdate = patch.autoUpdate;
|
|
6783
|
+
if (patch.pacingSessions !== void 0) s2.pacingSessions = clampPacingSessions(patch.pacingSessions);
|
|
6784
|
+
if (patch.voice !== void 0) s2.voice = voiceSettings(s2.voice, patch.voice);
|
|
6212
6785
|
this.save();
|
|
6213
|
-
this.broadcast({ type: "settings", settings:
|
|
6786
|
+
this.broadcast({ type: "settings", settings: s2 });
|
|
6214
6787
|
this.emitCeo();
|
|
6215
6788
|
setTimeout(() => this.schedule(), 200);
|
|
6216
|
-
return
|
|
6789
|
+
return s2;
|
|
6217
6790
|
}
|
|
6218
6791
|
/** The setup wizard: who you are, the company, and your CEO. */
|
|
6219
6792
|
setup(x) {
|
|
@@ -6264,8 +6837,8 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6264
6837
|
const ready = this.readyIssues(repo).map((r) => r.want);
|
|
6265
6838
|
const open = this.repoRt.get(repo.id).issues.map((i) => issueSpecialty(i.labels));
|
|
6266
6839
|
const rank = (a) => {
|
|
6267
|
-
const
|
|
6268
|
-
return [suits(a) ? 0 : 1, ready.filter((w) => w ===
|
|
6840
|
+
const s2 = a.specialty.toLowerCase();
|
|
6841
|
+
return [suits(a) ? 0 : 1, ready.filter((w) => w === s2).length, open.filter((w) => w === s2).length, a.desk];
|
|
6269
6842
|
};
|
|
6270
6843
|
return devs.map((a) => ({ a, r: rank(a) })).sort((x, y) => x.r[0] - y.r[0] || x.r[1] - y.r[1] || x.r[2] - y.r[2] || x.r[3] - y.r[3])[0].a;
|
|
6271
6844
|
}
|
|
@@ -6633,8 +7206,8 @@ ${job.text}` } : job;
|
|
|
6633
7206
|
void this.runCeoJob(a, job);
|
|
6634
7207
|
}
|
|
6635
7208
|
ceoPromptInput(a) {
|
|
6636
|
-
const
|
|
6637
|
-
return { name: a.name, company:
|
|
7209
|
+
const s2 = this.state.settings;
|
|
7210
|
+
return { name: a.name, company: s2.companyName, manager: s2.managerName, notesFile: path15.join(CEO_DIR, "NOTES.md"), sessionLimit: s2.sessionLimit, teamCap: s2.teamCap, hiring: s2.hiring };
|
|
6638
7211
|
}
|
|
6639
7212
|
async runCeoJob(a, job) {
|
|
6640
7213
|
const rt = this.agentRt.get(a.id);
|
|
@@ -6655,7 +7228,7 @@ ${job.text}` } : job;
|
|
|
6655
7228
|
this.emitAgent(a);
|
|
6656
7229
|
this.emitCeo();
|
|
6657
7230
|
this.save();
|
|
6658
|
-
await
|
|
7231
|
+
await fs13.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
6659
7232
|
if (a.status !== "working") {
|
|
6660
7233
|
this.state.ceo.job = null;
|
|
6661
7234
|
this.emitCeo();
|
|
@@ -6769,7 +7342,7 @@ ${t}`);
|
|
|
6769
7342
|
qa: this.state.qa.map((q) => [q.repoId, q.prNumber, q.status]),
|
|
6770
7343
|
requests: this.state.requests.filter((r) => r.status === "pending").map((r) => r.id)
|
|
6771
7344
|
};
|
|
6772
|
-
return
|
|
7345
|
+
return crypto7.createHash("sha1").update(JSON.stringify(data)).digest("hex");
|
|
6773
7346
|
}
|
|
6774
7347
|
maybeHeartbeat() {
|
|
6775
7348
|
const min = this.state.settings.ceoHeartbeatMin;
|
|
@@ -6882,7 +7455,7 @@ ${t}`);
|
|
|
6882
7455
|
return !BUSY.includes(a.status) ? null : a.task === "qa" ? `testing PR #${a.prNumber}` : a.task === "fix" ? `fixing PR #${a.prNumber}` : `issue #${a.issueNumber}`;
|
|
6883
7456
|
}
|
|
6884
7457
|
companyStatus() {
|
|
6885
|
-
const
|
|
7458
|
+
const s2 = this.state.settings;
|
|
6886
7459
|
const doing = (a) => this.agentDoing(a);
|
|
6887
7460
|
const jobDescription = (brief) => {
|
|
6888
7461
|
if (brief.length <= 400) return brief;
|
|
@@ -6959,9 +7532,9 @@ ${t}`);
|
|
|
6959
7532
|
{
|
|
6960
7533
|
company: {
|
|
6961
7534
|
ceo: this.ceo().name,
|
|
6962
|
-
hiring:
|
|
6963
|
-
teamCap:
|
|
6964
|
-
sessionLimit:
|
|
7535
|
+
hiring: s2.hiring === "auto" ? `auto-approved while a floor has fewer than ${s2.teamCap} people` : "the manager approves every proposal",
|
|
7536
|
+
teamCap: s2.teamCap,
|
|
7537
|
+
sessionLimit: s2.sessionLimit || "none",
|
|
6965
7538
|
sessionsRunning: this.running(),
|
|
6966
7539
|
usage: usageLabel(this.usageNow(), Date.now()),
|
|
6967
7540
|
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa },
|
|
@@ -7035,7 +7608,7 @@ ${t}`);
|
|
|
7035
7608
|
if (seated + waiting >= MAX_DESKS[role]) throw new Error(role === "qa" ? `The QA lab on floor ${repo.floor} is full.` : `Floor ${repo.floor} has no free desks.`);
|
|
7036
7609
|
const name = this.freeName(role);
|
|
7037
7610
|
const req = {
|
|
7038
|
-
id:
|
|
7611
|
+
id: crypto7.randomUUID(),
|
|
7039
7612
|
kind: "hire",
|
|
7040
7613
|
repoId: repo.id,
|
|
7041
7614
|
role,
|
|
@@ -7058,8 +7631,8 @@ ${t}`);
|
|
|
7058
7631
|
decidedBy: null
|
|
7059
7632
|
};
|
|
7060
7633
|
this.addRequest(req);
|
|
7061
|
-
const
|
|
7062
|
-
if (
|
|
7634
|
+
const s2 = this.state.settings;
|
|
7635
|
+
if (s2.hiring === "auto" && this.state.agents.filter((a) => a.repoId === repo.id).length < s2.teamCap) {
|
|
7063
7636
|
this.approveRequest(req.id, {}, "auto");
|
|
7064
7637
|
return `Hired ${req.name} as ${title} on floor ${repo.floor} (auto-approved; agent id ${req.agentId}).`;
|
|
7065
7638
|
}
|
|
@@ -7077,7 +7650,7 @@ ${t}`);
|
|
|
7077
7650
|
if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
|
|
7078
7651
|
checkPendingLimit(pending2.length);
|
|
7079
7652
|
const req = {
|
|
7080
|
-
id:
|
|
7653
|
+
id: crypto7.randomUUID(),
|
|
7081
7654
|
kind: "let-go",
|
|
7082
7655
|
repoId: repo.id,
|
|
7083
7656
|
role: a.role,
|
|
@@ -7113,11 +7686,11 @@ ${t}`);
|
|
|
7113
7686
|
const title = String(x.title ?? "").trim().slice(0, 120);
|
|
7114
7687
|
if (!title) throw new Error("An issue needs a title.");
|
|
7115
7688
|
const slug = specialtySlug(x.specialty);
|
|
7116
|
-
const
|
|
7689
|
+
const body2 = `${String(x.body ?? "").trim()}
|
|
7117
7690
|
|
|
7118
7691
|
---
|
|
7119
7692
|
_Filed by ${this.ceo().name}, the cubefarm CEO._`;
|
|
7120
|
-
const n = await this.backend.createIssue(repo.fullName, title,
|
|
7693
|
+
const n = await this.backend.createIssue(repo.fullName, title, body2, slug ? [specialtyLabel(slug)] : []);
|
|
7121
7694
|
this.ceoIssues.record(repo.id);
|
|
7122
7695
|
return `Filed #${n} on floor ${repo.floor}: ${title}${slug ? ` (routed to ${slug})` : ""}.`;
|
|
7123
7696
|
}
|
|
@@ -7238,6 +7811,7 @@ app.delete("/api/repos/:repo/preview", route((req) => swarm.stopPreview(repoId(r
|
|
|
7238
7811
|
app.post("/api/repos/:repo/pulls/:n/merge", route((req) => swarm.mergePull(repoId(req), num(req.params.n), req.body?.method ?? "squash")));
|
|
7239
7812
|
app.post("/api/repos/:repo/pulls/:n/close", route((req) => swarm.closePull(repoId(req), num(req.params.n))));
|
|
7240
7813
|
app.post("/api/repos/:repo/pulls/:n/qa", route((req) => swarm.sendToQa(repoId(req), num(req.params.n))));
|
|
7814
|
+
app.post("/api/repos/:repo/pulls/:n/fix", route((req) => swarm.sendBackToDev(repoId(req), num(req.params.n), parseSendBackNote(req.body))));
|
|
7241
7815
|
app.post(
|
|
7242
7816
|
"/api/repos/:repo/agents",
|
|
7243
7817
|
route(
|
|
@@ -7262,12 +7836,32 @@ app.post("/api/agents/:id/message", route((req) => swarm.message(String(req.para
|
|
|
7262
7836
|
app.get("/api/agents/:id/prompt", route((req) => swarm.agentPrompt(String(req.params.id))));
|
|
7263
7837
|
app.get("/api/agents/:id/screen", (req, res) => {
|
|
7264
7838
|
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
7839
|
res.setHeader("Cache-Control", "no-store");
|
|
7840
|
+
if (!shot) return void res.status(screenStatus(shot)).end();
|
|
7841
|
+
res.setHeader("Content-Type", shot.mime);
|
|
7268
7842
|
res.end(shot.data);
|
|
7269
7843
|
});
|
|
7270
7844
|
app.patch("/api/settings", route((req) => swarm.updateSettings(req.body ?? {})));
|
|
7845
|
+
var parse = (schema, value) => {
|
|
7846
|
+
const r = schema.safeParse(value);
|
|
7847
|
+
if (!r.success) throw new HttpError(400, r.error.issues.map((i) => `${i.path.join(".") || "request"}: ${i.message}`).join("; "));
|
|
7848
|
+
return r.data;
|
|
7849
|
+
};
|
|
7850
|
+
var sendAudio = (res, audio) => {
|
|
7851
|
+
res.setHeader("Content-Type", audio.subarray(0, 4).toString("latin1") === "RIFF" ? "audio/wav" : "audio/mpeg");
|
|
7852
|
+
res.setHeader("Cache-Control", "private, max-age=3600");
|
|
7853
|
+
res.end(audio);
|
|
7854
|
+
};
|
|
7855
|
+
app.put("/api/voice/key", route((req) => swarm.voice.setKey(parse(z3.object({ key: z3.string().max(400) }), req.body).key)));
|
|
7856
|
+
app.get("/api/voice/voices", route(() => swarm.voice.voices()));
|
|
7857
|
+
app.get(
|
|
7858
|
+
"/api/voice/messages/:id",
|
|
7859
|
+
route(async (req, res) => sendAudio(res, await swarm.voice.messageAudio(parse(z3.object({ id: z3.coerce.number().int().positive() }), req.params).id)))
|
|
7860
|
+
);
|
|
7861
|
+
app.get(
|
|
7862
|
+
"/api/voice/sample",
|
|
7863
|
+
route(async (req, res) => sendAudio(res, await swarm.voice.sampleAudio(parse(z3.object({ voiceId: z3.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "not a voice id").optional() }), req.query).voiceId)))
|
|
7864
|
+
);
|
|
7271
7865
|
app.post("/api/office/update", route((req) => swarm.updateOffice(req.body?.action)));
|
|
7272
7866
|
app.post("/api/ceo/message", route((req) => swarm.messageCeo(str(req.body.text))));
|
|
7273
7867
|
app.post("/api/ceo/review", route(() => swarm.requestReview()));
|
|
@@ -7277,10 +7871,10 @@ app.post(
|
|
|
7277
7871
|
route((req) => swarm.approveRequest(String(req.params.id), { name: str(req.body?.name) || void 0, model: typeof req.body?.model === "string" ? req.body.model : void 0, effort: typeof req.body?.effort === "string" ? req.body.effort : void 0 }))
|
|
7278
7872
|
);
|
|
7279
7873
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
7280
|
-
var dist =
|
|
7281
|
-
if (
|
|
7874
|
+
var dist = path16.resolve(import.meta.dirname, "../dist");
|
|
7875
|
+
if (fs14.existsSync(dist)) {
|
|
7282
7876
|
app.use(express.static(dist));
|
|
7283
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
7877
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path16.join(dist, "index.html")));
|
|
7284
7878
|
}
|
|
7285
7879
|
app.use((err, _req, res, _next) => {
|
|
7286
7880
|
const parserStatus = err?.status;
|