cubefarm 0.3.5 → 0.3.7
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/bin/cubefarm.js +9 -1
- package/dist/assets/ToyWorld-DQTMvcEl.js +2 -0
- package/dist/assets/{index-CeoPkKws.js → index-D3lYbosx.js} +30 -30
- package/dist/assets/{index-BU33ZUhQ.css → index-hi9vqi9h.css} +1 -1
- package/dist/assets/{rapier-JdQfFHwQ.js → rapier-DfUh5clU.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +281 -158
- package/dist-server/index.js.map +4 -4
- package/package.json +3 -1
- package/dist/assets/ToyWorld-KCq1UWWM.js +0 -2
package/dist-server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// server/index.ts
|
|
2
2
|
import http2 from "node:http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path14 from "node:path";
|
|
4
|
+
import fs12 from "node:fs";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
|
|
@@ -537,9 +537,46 @@ Write-Output $kill.Count`;
|
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
// server/agentRunner.ts
|
|
540
|
+
import fs4 from "node:fs";
|
|
541
|
+
import path4 from "node:path";
|
|
542
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
543
|
+
|
|
544
|
+
// server/browser.ts
|
|
540
545
|
import fs3 from "node:fs";
|
|
546
|
+
import { createRequire } from "node:module";
|
|
547
|
+
import os2 from "node:os";
|
|
541
548
|
import path3 from "node:path";
|
|
542
|
-
|
|
549
|
+
function playwrightMcpCli() {
|
|
550
|
+
try {
|
|
551
|
+
const pkgFile = createRequire(import.meta.url).resolve("@playwright/mcp/package.json");
|
|
552
|
+
const bin = JSON.parse(fs3.readFileSync(pkgFile, "utf8")).bin?.["playwright-mcp"] ?? "cli.js";
|
|
553
|
+
return path3.join(path3.dirname(pkgFile), bin);
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function playwrightMcpCommand(cli, initScript, outputDir, node = process.execPath) {
|
|
559
|
+
return { command: node, args: [cli, "--headless", "--isolated", ...outputDir ? ["--output-dir", outputDir] : [], "--init-script", initScript] };
|
|
560
|
+
}
|
|
561
|
+
function playwrightMcp(outputDir) {
|
|
562
|
+
const cli = playwrightMcpCli();
|
|
563
|
+
return cli ? playwrightMcpCommand(cli, path3.join(import.meta.dirname, "browser-init.js"), outputDir) : null;
|
|
564
|
+
}
|
|
565
|
+
function chromePaths(platform = process.platform, env = process.env, home = os2.homedir()) {
|
|
566
|
+
if (platform === "win32") {
|
|
567
|
+
return [env.LOCALAPPDATA, env.PROGRAMFILES, env["PROGRAMFILES(X86)"]].filter((d) => !!d).map((d) => path3.win32.join(d, "Google", "Chrome", "Application", "chrome.exe"));
|
|
568
|
+
}
|
|
569
|
+
if (platform === "darwin") return ["/Applications/Google Chrome.app", path3.posix.join(home, "Applications", "Google Chrome.app")];
|
|
570
|
+
return ["/opt/google/chrome/chrome", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"];
|
|
571
|
+
}
|
|
572
|
+
var hasChrome = () => chromePaths().some((p) => fs3.existsSync(p));
|
|
573
|
+
function browserProblem(mcp, chrome = hasChrome()) {
|
|
574
|
+
if (!mcp) return "\u26A0 The browser tool (@playwright/mcp) is not installed with cubefarm: reinstall it, or run npx cubefarm doctor";
|
|
575
|
+
if (!chrome) return "\u26A0 Google Chrome not found, so this session cannot test in a browser: install it, or run npx cubefarm doctor";
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// server/agentRunner.ts
|
|
543
580
|
var InputQueue = class {
|
|
544
581
|
items = [];
|
|
545
582
|
waiters = [];
|
|
@@ -571,7 +608,7 @@ function screenshotFile(cwd, text) {
|
|
|
571
608
|
const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
|
|
572
609
|
if (!file) return null;
|
|
573
610
|
try {
|
|
574
|
-
return { data:
|
|
611
|
+
return { data: fs4.readFileSync(path4.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
|
|
575
612
|
} catch {
|
|
576
613
|
return null;
|
|
577
614
|
}
|
|
@@ -580,17 +617,17 @@ var IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jp
|
|
|
580
617
|
function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
581
618
|
let names;
|
|
582
619
|
try {
|
|
583
|
-
names =
|
|
620
|
+
names = fs4.readdirSync(dir);
|
|
584
621
|
} catch {
|
|
585
622
|
return [];
|
|
586
623
|
}
|
|
587
624
|
const found2 = [];
|
|
588
625
|
for (const name of names.sort()) {
|
|
589
|
-
const mime = IMAGE_MIME[
|
|
590
|
-
const file =
|
|
626
|
+
const mime = IMAGE_MIME[path4.extname(name).toLowerCase()];
|
|
627
|
+
const file = path4.join(dir, name);
|
|
591
628
|
if (!mime || seen.has(name)) continue;
|
|
592
629
|
try {
|
|
593
|
-
if (now2 -
|
|
630
|
+
if (now2 - fs4.statSync(file).mtimeMs < 1e3) continue;
|
|
594
631
|
} catch {
|
|
595
632
|
continue;
|
|
596
633
|
}
|
|
@@ -602,7 +639,7 @@ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
|
602
639
|
function rel(cwd, p) {
|
|
603
640
|
const s = String(p ?? "");
|
|
604
641
|
if (!s) return "";
|
|
605
|
-
const r =
|
|
642
|
+
const r = path4.relative(cwd, path4.resolve(cwd, s));
|
|
606
643
|
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s;
|
|
607
644
|
}
|
|
608
645
|
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -729,8 +766,10 @@ function startSession(opts, callbacks) {
|
|
|
729
766
|
env.CLAUDE_AGENT_SDK_CLIENT_APP = `cubefarm/${VERSION}`;
|
|
730
767
|
const mcpServers = {};
|
|
731
768
|
if (opts.browserTesting) {
|
|
732
|
-
const
|
|
733
|
-
mcpServers.playwright =
|
|
769
|
+
const browser = playwrightMcp();
|
|
770
|
+
if (browser) mcpServers.playwright = browser;
|
|
771
|
+
const problem = browserProblem(browser);
|
|
772
|
+
if (problem) cb.log([{ kind: "error", text: problem }]);
|
|
734
773
|
}
|
|
735
774
|
if (opts.office) mcpServers.office = opts.office.server;
|
|
736
775
|
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"];
|
|
@@ -903,15 +942,15 @@ function startSession(opts, callbacks) {
|
|
|
903
942
|
|
|
904
943
|
// server/cliRunner.ts
|
|
905
944
|
import crypto3 from "node:crypto";
|
|
906
|
-
import
|
|
907
|
-
import
|
|
945
|
+
import fs7 from "node:fs";
|
|
946
|
+
import path8 from "node:path";
|
|
908
947
|
import { pathToFileURL } from "node:url";
|
|
909
948
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
910
949
|
|
|
911
950
|
// server/clis.ts
|
|
912
|
-
import
|
|
913
|
-
import { createRequire } from "node:module";
|
|
914
|
-
import
|
|
951
|
+
import fs5 from "node:fs";
|
|
952
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
953
|
+
import path5 from "node:path";
|
|
915
954
|
var CLIS = [
|
|
916
955
|
{ id: "claude", label: "Claude Code", command: "claude", integrated: true },
|
|
917
956
|
{ id: "codex", label: "Codex", command: "codex", integrated: false },
|
|
@@ -922,13 +961,13 @@ var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
|
|
|
922
961
|
var WIN = process.platform === "win32";
|
|
923
962
|
function resolveCommand(name, env = process.env) {
|
|
924
963
|
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
925
|
-
const dirs = (env[key] ?? "").split(
|
|
964
|
+
const dirs = (env[key] ?? "").split(path5.delimiter).filter(Boolean);
|
|
926
965
|
const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
|
|
927
966
|
for (const dir of dirs) {
|
|
928
967
|
for (const ext of exts) {
|
|
929
|
-
const file =
|
|
968
|
+
const file = path5.join(dir, name + ext.toLowerCase());
|
|
930
969
|
try {
|
|
931
|
-
const st =
|
|
970
|
+
const st = fs5.statSync(file);
|
|
932
971
|
if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
|
|
933
972
|
} catch {
|
|
934
973
|
}
|
|
@@ -939,16 +978,16 @@ function resolveCommand(name, env = process.env) {
|
|
|
939
978
|
function unwrapCmdShim(shimPath, text) {
|
|
940
979
|
const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
|
|
941
980
|
if (!line) return null;
|
|
942
|
-
const dir =
|
|
943
|
-
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) =>
|
|
981
|
+
const dir = path5.dirname(shimPath);
|
|
982
|
+
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path5.join(dir, m[1]));
|
|
944
983
|
const target = targets[targets.length - 1];
|
|
945
984
|
if (!target) return null;
|
|
946
985
|
if (/\.exe$/i.test(target)) return { file: target, args: [] };
|
|
947
986
|
if (!/\.[cm]?js$/i.test(target)) return null;
|
|
948
987
|
const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
|
|
949
988
|
const nodeFlags = line.includes('"%_prog%"') ? between.trim().split(/\s+/).filter((f) => f.startsWith("--")) : [];
|
|
950
|
-
const localNode =
|
|
951
|
-
return { file:
|
|
989
|
+
const localNode = path5.join(dir, "node.exe");
|
|
990
|
+
return { file: fs5.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
|
|
952
991
|
}
|
|
953
992
|
var bundled;
|
|
954
993
|
function bundledClaude() {
|
|
@@ -956,7 +995,7 @@ function bundledClaude() {
|
|
|
956
995
|
bundled = null;
|
|
957
996
|
let sdk;
|
|
958
997
|
try {
|
|
959
|
-
sdk =
|
|
998
|
+
sdk = createRequire2(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
960
999
|
} catch {
|
|
961
1000
|
return null;
|
|
962
1001
|
}
|
|
@@ -966,7 +1005,7 @@ function bundledClaude() {
|
|
|
966
1005
|
const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
967
1006
|
for (const target of targets) {
|
|
968
1007
|
try {
|
|
969
|
-
bundled =
|
|
1008
|
+
bundled = createRequire2(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
|
|
970
1009
|
return bundled;
|
|
971
1010
|
} catch {
|
|
972
1011
|
}
|
|
@@ -981,7 +1020,7 @@ function commandFor(id) {
|
|
|
981
1020
|
if (!found2) return null;
|
|
982
1021
|
if (WIN && /\.(cmd|bat)$/i.test(found2)) {
|
|
983
1022
|
try {
|
|
984
|
-
return unwrapCmdShim(found2,
|
|
1023
|
+
return unwrapCmdShim(found2, fs5.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
|
|
985
1024
|
} catch {
|
|
986
1025
|
return { file: found2, args: [] };
|
|
987
1026
|
}
|
|
@@ -1183,18 +1222,18 @@ export default async function CubefarmPlugin({ client } = {}) {
|
|
|
1183
1222
|
|
|
1184
1223
|
// server/ptyClient.ts
|
|
1185
1224
|
import crypto2 from "node:crypto";
|
|
1186
|
-
import
|
|
1225
|
+
import fs6 from "node:fs";
|
|
1187
1226
|
import net from "node:net";
|
|
1188
|
-
import
|
|
1227
|
+
import path7 from "node:path";
|
|
1189
1228
|
import { spawn } from "node:child_process";
|
|
1190
1229
|
|
|
1191
1230
|
// server/ptyProtocol.ts
|
|
1192
1231
|
import crypto from "node:crypto";
|
|
1193
|
-
import
|
|
1232
|
+
import path6 from "node:path";
|
|
1194
1233
|
var PTY_PROTOCOL = 1;
|
|
1195
1234
|
function hostSocket(home, platform = process.platform) {
|
|
1196
|
-
if (platform !== "win32") return
|
|
1197
|
-
const tag = crypto.createHash("sha1").update(
|
|
1235
|
+
if (platform !== "win32") return path6.join(home, "pty.sock");
|
|
1236
|
+
const tag = crypto.createHash("sha1").update(path6.resolve(home).toLowerCase()).digest("hex").slice(0, 12);
|
|
1198
1237
|
return `\\\\.\\pipe\\cubefarm-pty-${tag}`;
|
|
1199
1238
|
}
|
|
1200
1239
|
var encode = (m) => `${JSON.stringify(m)}
|
|
@@ -1325,7 +1364,7 @@ function connect(socket, secret) {
|
|
|
1325
1364
|
}
|
|
1326
1365
|
var runtimeFlags = () => process.execArgv.filter((a, i, all) => !/^--(inspect|watch|debug)/.test(a) && !/^--(inspect|watch|debug)/.test(all[i - 1] ?? ""));
|
|
1327
1366
|
function launch(socket, secretFile) {
|
|
1328
|
-
const entry =
|
|
1367
|
+
const entry = path7.join(import.meta.dirname, `ptyHost${path7.extname(import.meta.filename)}`);
|
|
1329
1368
|
spawn(process.execPath, [...runtimeFlags(), entry, "launch", socket, secretFile], { detached: true, stdio: "ignore", windowsHide: true }).once("error", () => void 0).unref();
|
|
1330
1369
|
}
|
|
1331
1370
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -1333,17 +1372,17 @@ async function startKeeper(home, onHook) {
|
|
|
1333
1372
|
hookHandler = onHook;
|
|
1334
1373
|
if (!nodePty) return [];
|
|
1335
1374
|
const socket = hostSocket(home);
|
|
1336
|
-
const secretFile =
|
|
1375
|
+
const secretFile = path7.join(home, "pty.secret");
|
|
1337
1376
|
try {
|
|
1338
|
-
if (!
|
|
1339
|
-
|
|
1340
|
-
|
|
1377
|
+
if (!fs6.existsSync(secretFile)) {
|
|
1378
|
+
fs6.mkdirSync(home, { recursive: true });
|
|
1379
|
+
fs6.writeFileSync(secretFile, crypto2.randomBytes(24).toString("hex"), { mode: 384 });
|
|
1341
1380
|
}
|
|
1342
1381
|
} catch (err) {
|
|
1343
1382
|
console.warn(` terminal keeper unavailable: ${err.message}`);
|
|
1344
1383
|
return [];
|
|
1345
1384
|
}
|
|
1346
|
-
const secret =
|
|
1385
|
+
const secret = fs6.readFileSync(secretFile, "utf8").trim();
|
|
1347
1386
|
let hello = await connect(socket, secret);
|
|
1348
1387
|
if (hello && hello.ready.version !== PTY_PROTOCOL) {
|
|
1349
1388
|
console.log(` replacing the terminal keeper (v${hello.ready.version}); the ${hello.ready.ptys.length} terminal(s) it held are stopped`);
|
|
@@ -1423,8 +1462,8 @@ function killTree(proc) {
|
|
|
1423
1462
|
}
|
|
1424
1463
|
|
|
1425
1464
|
// server/cliRunner.ts
|
|
1426
|
-
var SESSIONS_DIR =
|
|
1427
|
-
var BIN_DIR =
|
|
1465
|
+
var SESSIONS_DIR = path8.join(HOME_DIR, "sessions");
|
|
1466
|
+
var BIN_DIR = path8.join(HOME_DIR, "bin");
|
|
1428
1467
|
var FINISH_GRACE_MS = 3e3;
|
|
1429
1468
|
var BOOT_MS = 6e4;
|
|
1430
1469
|
var USAGE_WARN_PCT = 90;
|
|
@@ -1462,19 +1501,15 @@ async function handleMcp(token, req, res) {
|
|
|
1462
1501
|
var helpersWritten = false;
|
|
1463
1502
|
function writeHelpers() {
|
|
1464
1503
|
if (helpersWritten) return;
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1504
|
+
fs7.mkdirSync(BIN_DIR, { recursive: true });
|
|
1505
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "statusline.cjs"), STATUSLINE_SOURCE);
|
|
1506
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "notify.cjs"), NOTIFY_SOURCE);
|
|
1507
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "codex-hook.cjs"), CODEX_HOOK_SOURCE);
|
|
1508
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "opencode-plugin.mjs"), OPENCODE_PLUGIN_SOURCE);
|
|
1470
1509
|
helpersWritten = true;
|
|
1471
1510
|
}
|
|
1472
1511
|
var shellArg = (p) => `"${p.replaceAll("\\", "/")}"`;
|
|
1473
1512
|
var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop", "StopFailure", "Notification"];
|
|
1474
|
-
function playwrightServer(outputDir) {
|
|
1475
|
-
const args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--output-dir", outputDir, "--init-script", path7.join(import.meta.dirname, "browser-init.js")];
|
|
1476
|
-
return process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
|
|
1477
|
-
}
|
|
1478
1513
|
function toolOutput(response) {
|
|
1479
1514
|
const images = [];
|
|
1480
1515
|
const blocks = Array.isArray(response) ? response : Array.isArray(response?.content) ? response.content : null;
|
|
@@ -1521,7 +1556,7 @@ function endLive(live) {
|
|
|
1521
1556
|
live.proc?.kill();
|
|
1522
1557
|
live.proc = null;
|
|
1523
1558
|
live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
|
|
1524
|
-
|
|
1559
|
+
fs7.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
|
|
1525
1560
|
return live.cli === "codex" && live.resumeId ? codexThread("archive", live.resumeId, gone) : Promise.resolve();
|
|
1526
1561
|
}
|
|
1527
1562
|
var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
|
|
@@ -1605,7 +1640,7 @@ function startCliSession(opts, callbacks) {
|
|
|
1605
1640
|
const label = cliLabel(cli);
|
|
1606
1641
|
const token = crypto3.randomUUID();
|
|
1607
1642
|
const sessionId = opts.resumeSessionId ?? crypto3.randomUUID();
|
|
1608
|
-
const dir =
|
|
1643
|
+
const dir = path8.join(SESSIONS_DIR, token);
|
|
1609
1644
|
const started = Date.now();
|
|
1610
1645
|
const statusLine = `\u{1F3E2} cubefarm${opts.label ? ` \xB7 ${opts.label}` : ""}`;
|
|
1611
1646
|
const toolNames = /* @__PURE__ */ new Map();
|
|
@@ -1633,10 +1668,10 @@ function startCliSession(opts, callbacks) {
|
|
|
1633
1668
|
const collectShots = (all = false) => {
|
|
1634
1669
|
const l2 = live;
|
|
1635
1670
|
if (!l2 || cli === "claude" || !opts.browserTesting) return;
|
|
1636
|
-
for (const shot of newScreenshots(
|
|
1671
|
+
for (const shot of newScreenshots(path8.join(l2.dir, "browser"), l2.shots, all ? Infinity : Date.now())) {
|
|
1637
1672
|
l2.shots.add(shot.name);
|
|
1638
1673
|
try {
|
|
1639
|
-
cb.screenshot(
|
|
1674
|
+
cb.screenshot(fs7.readFileSync(shot.file), shot.mime);
|
|
1640
1675
|
} catch {
|
|
1641
1676
|
}
|
|
1642
1677
|
}
|
|
@@ -1707,7 +1742,7 @@ function startCliSession(opts, callbacks) {
|
|
|
1707
1742
|
const event = String(b.hook_event_name ?? "");
|
|
1708
1743
|
const sub = typeof b.agent_id === "string" && b.agent_id !== "";
|
|
1709
1744
|
if (cli === "claude" && !sub && b.session_id !== kept) {
|
|
1710
|
-
const id = resumableSession(b,
|
|
1745
|
+
const id = resumableSession(b, fs7.existsSync);
|
|
1711
1746
|
if (id) {
|
|
1712
1747
|
kept = id;
|
|
1713
1748
|
cb.sessionId(id);
|
|
@@ -1859,7 +1894,9 @@ function startCliSession(opts, callbacks) {
|
|
|
1859
1894
|
}
|
|
1860
1895
|
const keeper = keeperHookUrl();
|
|
1861
1896
|
const hookUrl = `${keeper ?? officeUrl}/api/hooks/${token}`;
|
|
1862
|
-
const browser = opts.browserTesting ?
|
|
1897
|
+
const browser = opts.browserTesting ? playwrightMcp(path8.join(dir, "browser")) : null;
|
|
1898
|
+
const browserIssue = opts.browserTesting ? browserProblem(browser) : null;
|
|
1899
|
+
if (browserIssue) log([{ kind: "error", text: browserIssue }]);
|
|
1863
1900
|
let prompt = opts.prompt;
|
|
1864
1901
|
if (opts.outputSchema) {
|
|
1865
1902
|
prompt += `
|
|
@@ -1870,21 +1907,21 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1870
1907
|
let files;
|
|
1871
1908
|
try {
|
|
1872
1909
|
writeHelpers();
|
|
1873
|
-
|
|
1910
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1874
1911
|
const settings = {
|
|
1875
1912
|
hooks: Object.fromEntries(HOOK_EVENTS.map((e) => [e, [{ hooks: [{ type: "http", url: hookUrl, timeout: keeper ? 120 : 30 }] }]])),
|
|
1876
|
-
statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(
|
|
1913
|
+
statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(path8.join(BIN_DIR, "statusline.cjs"))} ${shellArg(hookUrl)}`, padding: 0 }
|
|
1877
1914
|
};
|
|
1878
1915
|
const servers = {};
|
|
1879
1916
|
if (browser) servers.playwright = browser;
|
|
1880
1917
|
if (opts.office) servers.office = { type: "http", url: `${officeUrl}/api/mcp/${token}` };
|
|
1881
|
-
files = { settings:
|
|
1882
|
-
|
|
1883
|
-
if (files.mcp)
|
|
1884
|
-
|
|
1918
|
+
files = { settings: path8.join(dir, "settings.json"), mcp: Object.keys(servers).length ? path8.join(dir, "mcp.json") : null, system: path8.join(dir, "instructions.md") };
|
|
1919
|
+
fs7.writeFileSync(files.settings, JSON.stringify(settings, null, 2));
|
|
1920
|
+
if (files.mcp) fs7.writeFileSync(files.mcp, JSON.stringify({ mcpServers: servers }, null, 2));
|
|
1921
|
+
fs7.writeFileSync(files.system, opts.systemAppend);
|
|
1885
1922
|
if (prompt.length > INLINE_PROMPT_MAX) {
|
|
1886
|
-
const file =
|
|
1887
|
-
|
|
1923
|
+
const file = path8.join(dir, "task.md");
|
|
1924
|
+
fs7.writeFileSync(file, prompt);
|
|
1888
1925
|
prompt = `Your task is in ${file}. Read all of it first, then do what it says.`;
|
|
1889
1926
|
}
|
|
1890
1927
|
} catch (err) {
|
|
@@ -1904,9 +1941,9 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1904
1941
|
role: opts.role,
|
|
1905
1942
|
additionalDirectories: opts.additionalDirectories,
|
|
1906
1943
|
files,
|
|
1907
|
-
notify: { script:
|
|
1908
|
-
codexHook:
|
|
1909
|
-
plugin: pathToFileURL(
|
|
1944
|
+
notify: { script: path8.join(BIN_DIR, "notify.cjs"), url: hookUrl },
|
|
1945
|
+
codexHook: path8.join(BIN_DIR, "codex-hook.cjs"),
|
|
1946
|
+
plugin: pathToFileURL(path8.join(BIN_DIR, "opencode-plugin.mjs")).href,
|
|
1910
1947
|
browser
|
|
1911
1948
|
});
|
|
1912
1949
|
const env = {};
|
|
@@ -1972,9 +2009,9 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1972
2009
|
// server/previewRunner.ts
|
|
1973
2010
|
import { spawn as spawn2 } from "node:child_process";
|
|
1974
2011
|
import crypto4 from "node:crypto";
|
|
1975
|
-
import
|
|
2012
|
+
import fs8 from "node:fs/promises";
|
|
1976
2013
|
import net2 from "node:net";
|
|
1977
|
-
import
|
|
2014
|
+
import path9 from "node:path";
|
|
1978
2015
|
var PREVIEW_SLUG = "preview";
|
|
1979
2016
|
var PREVIEW_BRANCH = "swarm-preview";
|
|
1980
2017
|
var PREVIEW_TMP = ".preview-tmp";
|
|
@@ -2010,7 +2047,7 @@ async function portOpen(port) {
|
|
|
2010
2047
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2011
2048
|
async function readJson(file) {
|
|
2012
2049
|
try {
|
|
2013
|
-
return JSON.parse(await
|
|
2050
|
+
return JSON.parse(await fs8.readFile(file, "utf8"));
|
|
2014
2051
|
} catch {
|
|
2015
2052
|
return null;
|
|
2016
2053
|
}
|
|
@@ -2071,9 +2108,9 @@ var RealPreview = class {
|
|
|
2071
2108
|
this.check();
|
|
2072
2109
|
await ensureLocalExcludes(job.fullName).catch(() => void 0);
|
|
2073
2110
|
cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
|
|
2074
|
-
const tmp =
|
|
2075
|
-
await
|
|
2076
|
-
const pkg = await readJson(
|
|
2111
|
+
const tmp = path9.join(wt, PREVIEW_TMP);
|
|
2112
|
+
await fs8.mkdir(tmp, { recursive: true });
|
|
2113
|
+
const pkg = await readJson(path9.join(wt, "package.json"));
|
|
2077
2114
|
const command = job.command ?? defaultCommand(pkg);
|
|
2078
2115
|
if (!command) {
|
|
2079
2116
|
this.stopped = true;
|
|
@@ -2112,11 +2149,11 @@ var RealPreview = class {
|
|
|
2112
2149
|
}
|
|
2113
2150
|
/** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
|
|
2114
2151
|
async install(wt, env) {
|
|
2115
|
-
const lockFile =
|
|
2116
|
-
const lock = await
|
|
2117
|
-
const hash = crypto4.createHash("sha256").update(await
|
|
2118
|
-
const marker =
|
|
2119
|
-
if (await
|
|
2152
|
+
const lockFile = path9.join(wt, "package-lock.json");
|
|
2153
|
+
const lock = await fs8.readFile(lockFile, "utf8").catch(() => null);
|
|
2154
|
+
const hash = crypto4.createHash("sha256").update(await fs8.readFile(path9.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
|
|
2155
|
+
const marker = path9.join(wt, "node_modules", INSTALL_MARKER);
|
|
2156
|
+
if (await fs8.readFile(marker, "utf8").catch(() => "") === hash) {
|
|
2120
2157
|
this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2121
2158
|
return;
|
|
2122
2159
|
}
|
|
@@ -2130,7 +2167,7 @@ var RealPreview = class {
|
|
|
2130
2167
|
}
|
|
2131
2168
|
if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
|
|
2132
2169
|
this.child = null;
|
|
2133
|
-
await
|
|
2170
|
+
await fs8.writeFile(marker, hash).catch(() => void 0);
|
|
2134
2171
|
}
|
|
2135
2172
|
/** Run a command line through the platform shell; resolves with its exit code. */
|
|
2136
2173
|
spawn(cmd, cwd, env) {
|
|
@@ -2161,12 +2198,12 @@ var RealPreview = class {
|
|
|
2161
2198
|
};
|
|
2162
2199
|
var realPreviews = {
|
|
2163
2200
|
start: (job, cb) => new RealPreview(job, cb),
|
|
2164
|
-
hasDefault: async (fullName) =>
|
|
2201
|
+
hasDefault: async (fullName) => fs8.access(path9.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
2165
2202
|
};
|
|
2166
2203
|
|
|
2167
2204
|
// server/officeUpdate.ts
|
|
2168
|
-
import
|
|
2169
|
-
import
|
|
2205
|
+
import fs9 from "node:fs/promises";
|
|
2206
|
+
import path10 from "node:path";
|
|
2170
2207
|
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
2171
2208
|
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
2172
2209
|
function underLauncher(proc = process) {
|
|
@@ -2203,10 +2240,10 @@ function parseLastUpdate(raw) {
|
|
|
2203
2240
|
}
|
|
2204
2241
|
}
|
|
2205
2242
|
async function takeLastUpdate(home) {
|
|
2206
|
-
const file =
|
|
2207
|
-
const raw = await
|
|
2243
|
+
const file = path10.join(home, "last-update.json");
|
|
2244
|
+
const raw = await fs9.readFile(file, "utf8").catch(() => null);
|
|
2208
2245
|
if (raw === null) return null;
|
|
2209
|
-
await
|
|
2246
|
+
await fs9.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
2210
2247
|
return parseLastUpdate(raw);
|
|
2211
2248
|
}
|
|
2212
2249
|
function lastUpdateMessage(u, commits) {
|
|
@@ -2214,8 +2251,8 @@ function lastUpdateMessage(u, commits) {
|
|
|
2214
2251
|
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
2215
2252
|
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
2216
2253
|
}
|
|
2217
|
-
var OFFICE_DIR =
|
|
2218
|
-
var isSamePath = (a, b) => process.platform === "win32" ?
|
|
2254
|
+
var OFFICE_DIR = path10.resolve(import.meta.dirname, "..");
|
|
2255
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path10.resolve(a).toLowerCase() === path10.resolve(b).toLowerCase() : path10.resolve(a) === path10.resolve(b);
|
|
2219
2256
|
var realOffice = {
|
|
2220
2257
|
launcher: underLauncher(),
|
|
2221
2258
|
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
@@ -2273,7 +2310,7 @@ var realBackend = {
|
|
|
2273
2310
|
// server/demo.ts
|
|
2274
2311
|
import crypto5 from "node:crypto";
|
|
2275
2312
|
import http from "node:http";
|
|
2276
|
-
import
|
|
2313
|
+
import path11 from "node:path";
|
|
2277
2314
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
2278
2315
|
function issue(n, title, body, fullName, labels = []) {
|
|
2279
2316
|
return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
@@ -2326,7 +2363,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
2326
2363
|
</svg>`;
|
|
2327
2364
|
}
|
|
2328
2365
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
2329
|
-
const branch =
|
|
2366
|
+
const branch = path11.basename(opts.cwd);
|
|
2330
2367
|
const hue = issueNumber * 67 % 360;
|
|
2331
2368
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
2332
2369
|
const port = 5200 + issueNumber % 50;
|
|
@@ -2386,13 +2423,13 @@ function qaScript(cb, pr, title, round) {
|
|
|
2386
2423
|
[{ kind: "tool", tool: "mcp__playwright__browser_console_messages", text: "\u23FA \u{1F310} console_messages" }, { kind: "result", text: " \u23BF No errors" }]
|
|
2387
2424
|
];
|
|
2388
2425
|
}
|
|
2389
|
-
function fixScript(pr) {
|
|
2426
|
+
function fixScript(pr, pushes) {
|
|
2390
2427
|
return [
|
|
2391
2428
|
[{ kind: "text", text: `\u25CF Reading the QA report for PR #${pr}. The toolbar overflows on phones; I'll let it wrap.` }],
|
|
2392
2429
|
[{ kind: "tool", tool: "Read", text: "\u23FA Read src/styles.css" }, { kind: "result", text: " \u23BF Read 212 lines" }],
|
|
2393
2430
|
[{ kind: "tool", tool: "Edit", text: "\u23FA Edit src/styles.css" }, { kind: "result", text: " \u23BF Updated" }],
|
|
2394
2431
|
[{ kind: "tool", tool: "Bash", text: "\u23FA $ npm test -- --run" }, { kind: "result", text: " \u23BF Test Files 8 passed (8)" }],
|
|
2395
|
-
[{ kind: "tool", tool: "Bash", text: '\u23FA $ git commit -am "fix: wrap toolbar on narrow screens" && git push origin HEAD' }, { kind: "result", text: " \u23BF pushed" }]
|
|
2432
|
+
pushes ? [{ kind: "tool", tool: "Bash", text: '\u23FA $ git commit -am "fix: wrap toolbar on narrow screens" && git push origin HEAD' }, { kind: "result", text: " \u23BF pushed" }] : [{ kind: "tool", tool: "Bash", text: "\u23FA $ git status --short" }, { kind: "result", text: " \u23BF M src/styles.css" }]
|
|
2396
2433
|
];
|
|
2397
2434
|
}
|
|
2398
2435
|
function fakeSession(opts, cb, fullName) {
|
|
@@ -2404,11 +2441,12 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2404
2441
|
const number = Number((kind === "issue" ? issueMatch?.[1] : prMatch?.[1]) ?? 0);
|
|
2405
2442
|
const title = (kind === "issue" ? issueMatch?.[2] : prMatch?.[2])?.trim() ?? "follow-up";
|
|
2406
2443
|
const round = Number(opts.prompt.match(/QA round (\d+)/)?.[1] ?? 1);
|
|
2444
|
+
const pushes = kind !== "fix" || !/FAILED|taking over pull request/.test(opts.prompt) || Math.random() > 0.25;
|
|
2407
2445
|
const header = [
|
|
2408
2446
|
{ kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort` },
|
|
2409
2447
|
{ kind: "system", text: ` cwd ${opts.cwd}` }
|
|
2410
2448
|
];
|
|
2411
|
-
const body = kind === "qa" ? qaScript(cb, number, title, round) : kind === "fix" ? fixScript(number) : devScript(opts, cb, number, title);
|
|
2449
|
+
const body = kind === "qa" ? qaScript(cb, number, title, round) : kind === "fix" ? fixScript(number, pushes) : devScript(opts, cb, number, title);
|
|
2412
2450
|
const script = [header, ...body];
|
|
2413
2451
|
const finish = () => {
|
|
2414
2452
|
const costUsd = 0.3 + Math.random();
|
|
@@ -2445,13 +2483,13 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2445
2483
|
}
|
|
2446
2484
|
if (kind === "fix") {
|
|
2447
2485
|
const pr = repos.get(fullName)?.pulls.find((p) => p.number === number);
|
|
2448
|
-
if (pr) {
|
|
2486
|
+
if (pr && pushes) {
|
|
2449
2487
|
pr.headSha = fakeSha();
|
|
2450
2488
|
pr.mergeState = "CLEAN";
|
|
2451
2489
|
pr.mergeable = "MERGEABLE";
|
|
2452
2490
|
runChecks(pr, false);
|
|
2453
2491
|
}
|
|
2454
|
-
cb.log([{ kind: "text", text: `\u25CF Fixed PR #${number} and pushed. Ready for another QA round.` }]);
|
|
2492
|
+
cb.log([{ kind: "text", text: pushes ? `\u25CF Fixed PR #${number} and pushed. Ready for another QA round.` : `\u25CF The toolbar wraps on narrow screens now. Ready for another QA round.` }]);
|
|
2455
2493
|
cb.finished({ ok: true, text: "", costUsd, turns, errors: [] });
|
|
2456
2494
|
return;
|
|
2457
2495
|
}
|
|
@@ -2464,7 +2502,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2464
2502
|
number: n,
|
|
2465
2503
|
title,
|
|
2466
2504
|
url,
|
|
2467
|
-
headRefName: `swarm/issue-${number}-${
|
|
2505
|
+
headRefName: `swarm/issue-${number}-${path11.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
2468
2506
|
state: "OPEN",
|
|
2469
2507
|
isDraft: false,
|
|
2470
2508
|
mergeable: "MERGEABLE",
|
|
@@ -2911,7 +2949,8 @@ function ceoSession(opts, cb) {
|
|
|
2911
2949
|
const s = JSON.parse(await office.call("company_status", {}));
|
|
2912
2950
|
const people = s.floors.reduce((n, f) => n + f.team.length, 0);
|
|
2913
2951
|
const issues = s.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
2914
|
-
|
|
2952
|
+
const free = s.floors.reduce((n, f) => n + f.seats.dev.free + f.seats.qa.free, 0);
|
|
2953
|
+
cb.log([{ kind: "result", text: ` \u23BF ${s.floors.length} floors \xB7 ${people} people \xB7 ${free} free seats \xB7 ${issues} open issues \xB7 ${s.pendingProposals.length} proposals pending` }]);
|
|
2915
2954
|
return s;
|
|
2916
2955
|
};
|
|
2917
2956
|
const use = async (name, args) => {
|
|
@@ -3102,8 +3141,8 @@ Acceptance criteria:
|
|
|
3102
3141
|
}
|
|
3103
3142
|
|
|
3104
3143
|
// server/swarm.ts
|
|
3105
|
-
import
|
|
3106
|
-
import
|
|
3144
|
+
import fs11 from "node:fs/promises";
|
|
3145
|
+
import path13 from "node:path";
|
|
3107
3146
|
import crypto6 from "node:crypto";
|
|
3108
3147
|
|
|
3109
3148
|
// server/ceo.ts
|
|
@@ -3188,7 +3227,7 @@ function createOfficeTools(h) {
|
|
|
3188
3227
|
const defs = [
|
|
3189
3228
|
tool(
|
|
3190
3229
|
"company_status",
|
|
3191
|
-
"Everything about the company right now: settings, every floor (repo, clone path, brief, profile, QA brief), its team, backlog, pull requests and QA, pending proposals and recent decisions by the manager. Call this first.",
|
|
3230
|
+
"Everything about the company right now: settings, every floor (repo, clone path, brief, profile, QA brief), its free seats, team, backlog, pull requests and QA, pending proposals and recent decisions by the manager. Call this first.",
|
|
3192
3231
|
{},
|
|
3193
3232
|
() => run2(() => h.companyStatus())
|
|
3194
3233
|
),
|
|
@@ -3280,6 +3319,12 @@ function createOfficeTools(h) {
|
|
|
3280
3319
|
}
|
|
3281
3320
|
};
|
|
3282
3321
|
}
|
|
3322
|
+
var FLOOR_DESKS = { dev: 12, qa: 3 };
|
|
3323
|
+
var MAX_PENDING_PROPOSALS = FLOOR_DESKS.dev + FLOOR_DESKS.qa;
|
|
3324
|
+
var seatCount = (total, taken, proposed) => ({ total, taken, proposed, free: Math.max(0, total - taken - proposed) });
|
|
3325
|
+
function checkPendingLimit(pending2, max = MAX_PENDING_PROPOSALS) {
|
|
3326
|
+
if (pending2 >= max) throw new Error(`${pending2} proposals are already waiting for the manager; propose the rest after they decide.`);
|
|
3327
|
+
}
|
|
3283
3328
|
function ceoSystemPrompt(o) {
|
|
3284
3329
|
const manager = o.manager ? `the manager, ${o.manager}` : "the human manager";
|
|
3285
3330
|
return [
|
|
@@ -3288,7 +3333,7 @@ function ceoSystemPrompt(o) {
|
|
|
3288
3333
|
"",
|
|
3289
3334
|
"Your job is to run the company, not to write code:",
|
|
3290
3335
|
"- Understand each project: what it is, its stack, how far along it is, and what kind of people it needs. Projects differ a lot. A static marketing site, a 3D browser game and a REST API need different specialists and different QA.",
|
|
3291
|
-
`- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: agents on the same coding agent share one subscription's usage limits${o.sessionLimit ? ` and at most ${o.sessionLimit} sessions run at once` : ""}, so a floor rarely needs more than ${o.teamCap} people. Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
|
|
3336
|
+
`- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: agents on the same coding agent share one subscription's usage limits${o.sessionLimit ? ` and at most ${o.sessionLimit} sessions run at once` : ""}, so a floor rarely needs more than ${o.teamCap} people. When the manager asks for a bigger team, follow that, up to the floor's free seats (seats in company_status). Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
|
|
3292
3337
|
"- Plan the work: turn a floor's brief into small, well-specified GitHub issues, one agent-session each, with acceptance criteria. Route each to a specialty. The office hands issues out itself: a free specialist gets first pick of their specialty, and otherwise any free developer takes the next issue that can start, so a specialty is a preference, not a lock.",
|
|
3293
3338
|
"- Write each floor's QA brief: what QA testers must check for this kind of project (for a 3D game: the canvas renders, controls respond, frame rate is smooth; for a website: links, phone layout, accessibility; for an API: status codes, validation, error cases).",
|
|
3294
3339
|
"",
|
|
@@ -3443,6 +3488,26 @@ function specialtySlug(s) {
|
|
|
3443
3488
|
}
|
|
3444
3489
|
var specialtyLabel = (slug) => `swarm:${slug}`;
|
|
3445
3490
|
|
|
3491
|
+
// server/fixOutcome.ts
|
|
3492
|
+
var MAX_FIX_FAILURES = 2;
|
|
3493
|
+
function fixOutcome(rec, head, minutes, counts = true) {
|
|
3494
|
+
const pr = rec.prNumber;
|
|
3495
|
+
const before = rec.fixReason === "conflict" ? rec.passedSha ?? rec.testedSha : rec.testedSha;
|
|
3496
|
+
if (rec.fixReason !== "checks" && head && before && head === before) {
|
|
3497
|
+
const sessionFailures = rec.sessionFailures + (counts ? 1 : 0);
|
|
3498
|
+
const needsHuman = sessionFailures >= MAX_FIX_FAILURES;
|
|
3499
|
+
return {
|
|
3500
|
+
set: needsHuman ? { status: "needs-human", sessionFailures, mergeNote: rec.fixReason === "conflict" ? "the conflict was never resolved" : "the fix was never pushed" } : { status: "failed", sessionFailures },
|
|
3501
|
+
log: { kind: "error", text: `\u2717 No new commits were pushed for PR #${pr}` },
|
|
3502
|
+
pushed: false
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
if (rec.fixReason === "checks" || rec.fixReason === "conflict") {
|
|
3506
|
+
return { set: { status: "passed", sessionFailures: 0, mergeNote: "waiting for fresh checks" }, log: { kind: "done", text: `\u2714 PR #${pr} fixed in ${minutes}m. Back in line to merge.` }, pushed: true };
|
|
3507
|
+
}
|
|
3508
|
+
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 };
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3446
3511
|
// server/httpError.ts
|
|
3447
3512
|
var HttpError = class extends Error {
|
|
3448
3513
|
constructor(status, message) {
|
|
@@ -3491,6 +3556,27 @@ function orphanedQa(qa, agents, busy) {
|
|
|
3491
3556
|
);
|
|
3492
3557
|
}
|
|
3493
3558
|
|
|
3559
|
+
// server/restartRecovery.ts
|
|
3560
|
+
function followKeptCli(a, cli) {
|
|
3561
|
+
if (!cli?.busy || a.status === "preparing") return false;
|
|
3562
|
+
return !(a.sessionId && cli.resumeId && cli.resumeId !== a.sessionId);
|
|
3563
|
+
}
|
|
3564
|
+
function resumesAfterRestart(a, preparing, demo) {
|
|
3565
|
+
return !demo && !preparing && a.task !== "qa" && !!a.sessionId && !!a.branch;
|
|
3566
|
+
}
|
|
3567
|
+
function resumeNote(a, fix) {
|
|
3568
|
+
const yours = "Uncommitted changes in your worktree are yours to finish.";
|
|
3569
|
+
if (a.task === "fix" && a.prNumber && a.branch) {
|
|
3570
|
+
const job = fix?.fixReason === "conflict" ? `resolving the merge conflicts on PR #${a.prNumber}` : fix?.fixReason === "checks" ? `fixing the failing checks on PR #${a.prNumber}` : `fixing PR #${a.prNumber}${fix ? ` after QA round ${fix.round}` : ""}`;
|
|
3571
|
+
return `The office server restarted while you were ${job}. Finish the fix, commit, and push with: git push origin HEAD:${a.branch}. ${yours}`;
|
|
3572
|
+
}
|
|
3573
|
+
if (a.task === "issue" && a.issueNumber && a.branch) {
|
|
3574
|
+
const pr = a.prNumber ? `push with: git push origin ${a.branch} to update PR #${a.prNumber}` : `push with: git push -u origin ${a.branch}, and open the pull request with "Closes #${a.issueNumber}" (or push to it if it's already open)`;
|
|
3575
|
+
return `The office server restarted while you were working on issue #${a.issueNumber}${a.issueTitle ? ` (${a.issueTitle})` : ""} on branch ${a.branch}. Finish it, commit, ${pr}. ${yours}`;
|
|
3576
|
+
}
|
|
3577
|
+
return "The office server restarted while you were working. Check the state of your worktree and continue where you left off.";
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3494
3580
|
// server/previews.ts
|
|
3495
3581
|
var PREVIEW_BASE_PORT = 6300;
|
|
3496
3582
|
var LOG_KEEP = 200;
|
|
@@ -3729,8 +3815,8 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
3729
3815
|
}
|
|
3730
3816
|
|
|
3731
3817
|
// server/terminal.ts
|
|
3732
|
-
import
|
|
3733
|
-
import
|
|
3818
|
+
import fs10 from "node:fs/promises";
|
|
3819
|
+
import path12 from "node:path";
|
|
3734
3820
|
import xtermHeadless from "@xterm/headless";
|
|
3735
3821
|
import xtermSerialize from "@xterm/addon-serialize";
|
|
3736
3822
|
var { Terminal } = xtermHeadless;
|
|
@@ -3824,12 +3910,12 @@ var AgentTerminal = class {
|
|
|
3824
3910
|
async save(file) {
|
|
3825
3911
|
this.dirty = false;
|
|
3826
3912
|
await this.flush();
|
|
3827
|
-
await
|
|
3828
|
-
await
|
|
3829
|
-
await
|
|
3913
|
+
await fs10.mkdir(path12.dirname(file), { recursive: true });
|
|
3914
|
+
await fs10.writeFile(`${file}.tmp`, this.snapshot());
|
|
3915
|
+
await fs10.rename(`${file}.tmp`, file);
|
|
3830
3916
|
}
|
|
3831
3917
|
async load(file) {
|
|
3832
|
-
const data = await
|
|
3918
|
+
const data = await fs10.readFile(file, "utf8").catch(() => "");
|
|
3833
3919
|
if (!data) return;
|
|
3834
3920
|
this.term.write(data);
|
|
3835
3921
|
this.note("(the office restarted)");
|
|
@@ -3959,16 +4045,15 @@ var FEMININE_NAMES = new Set(
|
|
|
3959
4045
|
);
|
|
3960
4046
|
var lookFor = (name) => FEMININE_NAMES.has(name.trim().split(/\s+/)[0].toLowerCase()) ? "feminine" : "masculine";
|
|
3961
4047
|
var LOOKS = ["feminine", "masculine"];
|
|
3962
|
-
var MAX_DESKS = {
|
|
4048
|
+
var MAX_DESKS = { ...FLOOR_DESKS, ceo: 1 };
|
|
3963
4049
|
var MAX_QA_ROUNDS = 3;
|
|
3964
4050
|
var DEFAULT_MODEL = "claude-opus-5-5";
|
|
3965
4051
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
3966
4052
|
var CEO_MODEL = "claude-opus-5-5";
|
|
3967
4053
|
var CEO_EFFORT = "xhigh";
|
|
3968
4054
|
var CEO_NAME = "Morgan";
|
|
3969
|
-
var CEO_DIR =
|
|
3970
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
3971
|
-
var MAX_PENDING_REQUESTS = 8;
|
|
4055
|
+
var CEO_DIR = path13.join(HOME_DIR, "ceo");
|
|
4056
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path13.resolve(import.meta.dirname, ".."));
|
|
3972
4057
|
var MAX_ISSUES_PER_JOB = 12;
|
|
3973
4058
|
var KEEP_MESSAGES = 200;
|
|
3974
4059
|
var KEEP_DECIDED_REQUESTS = 40;
|
|
@@ -3980,14 +4065,14 @@ var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
|
3980
4065
|
var MAX_ISSUE_FAILURES = 2;
|
|
3981
4066
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
3982
4067
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
3983
|
-
var SCREENS_DIR =
|
|
4068
|
+
var SCREENS_DIR = path13.join(HOME_DIR, "screens");
|
|
3984
4069
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
3985
|
-
var screenFile = (agentId, mime) =>
|
|
4070
|
+
var screenFile = (agentId, mime) => path13.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
3986
4071
|
async function loadScreen(agentId) {
|
|
3987
4072
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
3988
|
-
const file =
|
|
4073
|
+
const file = path13.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
3989
4074
|
try {
|
|
3990
|
-
const [data, stat] = await Promise.all([
|
|
4075
|
+
const [data, stat] = await Promise.all([fs11.readFile(file), fs11.stat(file)]);
|
|
3991
4076
|
return { data, mime, at: stat.mtimeMs };
|
|
3992
4077
|
} catch {
|
|
3993
4078
|
}
|
|
@@ -3995,10 +4080,10 @@ async function loadScreen(agentId) {
|
|
|
3995
4080
|
return null;
|
|
3996
4081
|
}
|
|
3997
4082
|
async function removeScreens(agentId) {
|
|
3998
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
4083
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs11.rm(path13.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
3999
4084
|
}
|
|
4000
|
-
var TERMINALS_DIR =
|
|
4001
|
-
var terminalFile = (agentId) =>
|
|
4085
|
+
var TERMINALS_DIR = path13.join(HOME_DIR, "terminals");
|
|
4086
|
+
var terminalFile = (agentId) => path13.join(TERMINALS_DIR, `${agentId}.ansi`);
|
|
4002
4087
|
var TERMINAL_SAVE_MS = 2e4;
|
|
4003
4088
|
var QA_SCHEMA = {
|
|
4004
4089
|
type: "object",
|
|
@@ -4152,7 +4237,7 @@ var Swarm = class {
|
|
|
4152
4237
|
// ---------- lifecycle ----------
|
|
4153
4238
|
async init() {
|
|
4154
4239
|
try {
|
|
4155
|
-
const raw = await
|
|
4240
|
+
const raw = await fs11.readFile(STATE_FILE, "utf8");
|
|
4156
4241
|
const loaded = JSON.parse(raw);
|
|
4157
4242
|
this.state = {
|
|
4158
4243
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -4227,12 +4312,14 @@ var Swarm = class {
|
|
|
4227
4312
|
return [];
|
|
4228
4313
|
});
|
|
4229
4314
|
const carryOn = [];
|
|
4315
|
+
const preparing = /* @__PURE__ */ new Set();
|
|
4230
4316
|
for (const a of this.state.agents) {
|
|
4231
4317
|
if (!BUSY.includes(a.status)) continue;
|
|
4232
|
-
if (back.
|
|
4318
|
+
if (followKeptCli(a, back.find((c) => c.agentId === a.id))) {
|
|
4233
4319
|
carryOn.push(a);
|
|
4234
4320
|
continue;
|
|
4235
4321
|
}
|
|
4322
|
+
if (a.status === "preparing") preparing.add(a);
|
|
4236
4323
|
a.status = "stopped";
|
|
4237
4324
|
a.lastError = "The swarm server restarted while this agent was working.";
|
|
4238
4325
|
interrupted.push(a);
|
|
@@ -4268,7 +4355,7 @@ var Swarm = class {
|
|
|
4268
4355
|
this.previews.clearOrphans(this.state.repos)
|
|
4269
4356
|
]);
|
|
4270
4357
|
for (const r of this.state.repos) void this.previews.refreshDefault(r);
|
|
4271
|
-
this.recover(interrupted);
|
|
4358
|
+
this.recover(interrupted, preparing);
|
|
4272
4359
|
for (const rec of orphanedQa(this.state.qa, this.state.agents, BUSY)) this.setQa(rec, { status: "queued", qaAgentId: null });
|
|
4273
4360
|
this.officeHead = await this.backend.office.head();
|
|
4274
4361
|
const updated = await this.backend.office.takeLastUpdate().catch(() => null);
|
|
@@ -4283,22 +4370,23 @@ var Swarm = class {
|
|
|
4283
4370
|
this.save();
|
|
4284
4371
|
setTimeout(() => this.schedule(), 1e3);
|
|
4285
4372
|
}
|
|
4286
|
-
/**
|
|
4287
|
-
|
|
4373
|
+
/**
|
|
4374
|
+
* Agents cut off by a server restart pick their session back up, told what they were doing. QA, demo agents and
|
|
4375
|
+
* tasks still being prepared start over from the queue.
|
|
4376
|
+
*/
|
|
4377
|
+
recover(agents, preparing) {
|
|
4288
4378
|
for (const a of agents) {
|
|
4289
|
-
|
|
4379
|
+
const fix = a.task === "fix" ? this.state.qa.find((q) => q.devAgentId === a.id && q.status === "fixing") : void 0;
|
|
4380
|
+
if (!resumesAfterRestart(a, preparing.has(a), this.backend.demo)) {
|
|
4290
4381
|
this.appendLog(a, [{ kind: "system", text: "\u21BA The office server restarted. Starting over from the queue." }]);
|
|
4291
4382
|
const rec = a.task === "qa" ? this.state.qa.find((q) => q.qaAgentId === a.id && q.status === "testing") : void 0;
|
|
4292
4383
|
if (rec) this.setQa(rec, { status: "queued" });
|
|
4293
|
-
const fix = a.task === "fix" ? this.state.qa.find((q) => q.devAgentId === a.id && q.status === "fixing") : void 0;
|
|
4294
4384
|
if (fix) this.setQa(fix, { status: "failed" });
|
|
4295
4385
|
this.clearTask(a);
|
|
4296
4386
|
continue;
|
|
4297
4387
|
}
|
|
4298
4388
|
if (this.slotsFull()) continue;
|
|
4299
|
-
void this.message(a.id,
|
|
4300
|
-
(err) => console.warn(`could not resume ${a.name}`, err)
|
|
4301
|
-
);
|
|
4389
|
+
void this.message(a.id, resumeNote(a, fix)).catch((err) => console.warn(`could not resume ${a.name}`, err));
|
|
4302
4390
|
}
|
|
4303
4391
|
}
|
|
4304
4392
|
// ---------- views ----------
|
|
@@ -4470,10 +4558,10 @@ var Swarm = class {
|
|
|
4470
4558
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
4471
4559
|
this.saveTimer = null;
|
|
4472
4560
|
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
4473
|
-
await
|
|
4561
|
+
await fs11.mkdir(path13.dirname(STATE_FILE), { recursive: true });
|
|
4474
4562
|
const tmp = `${STATE_FILE}.tmp`;
|
|
4475
|
-
await
|
|
4476
|
-
await
|
|
4563
|
+
await fs11.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
4564
|
+
await fs11.rename(tmp, STATE_FILE);
|
|
4477
4565
|
}
|
|
4478
4566
|
// ---------- lookups ----------
|
|
4479
4567
|
repo(id) {
|
|
@@ -4555,7 +4643,7 @@ var Swarm = class {
|
|
|
4555
4643
|
const name = fullName.split("/")[1];
|
|
4556
4644
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
4557
4645
|
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.`);
|
|
4558
|
-
return
|
|
4646
|
+
return path13.join(root, name);
|
|
4559
4647
|
}
|
|
4560
4648
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
4561
4649
|
async createRepo(name, opts) {
|
|
@@ -4567,11 +4655,11 @@ var Swarm = class {
|
|
|
4567
4655
|
}
|
|
4568
4656
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
4569
4657
|
async listProjectFolders(dir) {
|
|
4570
|
-
const root = dir?.trim() ?
|
|
4658
|
+
const root = dir?.trim() ? path13.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
4571
4659
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
4572
4660
|
throw new HttpError(400, err.message);
|
|
4573
4661
|
});
|
|
4574
|
-
const same = (a, b) =>
|
|
4662
|
+
const same = (a, b) => path13.resolve(a).toLowerCase() === path13.resolve(b).toLowerCase();
|
|
4575
4663
|
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;
|
|
4576
4664
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
4577
4665
|
}
|
|
@@ -4751,7 +4839,7 @@ var Swarm = class {
|
|
|
4751
4839
|
rt.folderSync = status;
|
|
4752
4840
|
this.emitRepo(repo);
|
|
4753
4841
|
if (status && status !== before) {
|
|
4754
|
-
const name = repo.localPath ?
|
|
4842
|
+
const name = repo.localPath ? path13.basename(repo.localPath) : repo.fullName;
|
|
4755
4843
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
4756
4844
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
4757
4845
|
}
|
|
@@ -4975,7 +5063,7 @@ var Swarm = class {
|
|
|
4975
5063
|
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
4976
5064
|
this.agentRt.get(id)?.terminal?.dispose();
|
|
4977
5065
|
void removeScreens(id);
|
|
4978
|
-
void
|
|
5066
|
+
void fs11.rm(terminalFile(id), { force: true }).catch(() => void 0);
|
|
4979
5067
|
for (const q of this.state.qa) {
|
|
4980
5068
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
4981
5069
|
if (q.devAgentId === id) q.devAgentId = null;
|
|
@@ -5078,7 +5166,7 @@ var Swarm = class {
|
|
|
5078
5166
|
}
|
|
5079
5167
|
// ---------- terminals ----------
|
|
5080
5168
|
async loadTerminal(agentId) {
|
|
5081
|
-
if (!await
|
|
5169
|
+
if (!await fs11.stat(terminalFile(agentId)).catch(() => null)) return null;
|
|
5082
5170
|
const t = this.newTerminal(agentId);
|
|
5083
5171
|
await t.load(terminalFile(agentId));
|
|
5084
5172
|
return t;
|
|
@@ -5244,7 +5332,7 @@ Note from the manager: ${note}` : ""
|
|
|
5244
5332
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
5245
5333
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
5246
5334
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
5247
|
-
void removeScreens(a.id).then(() =>
|
|
5335
|
+
void removeScreens(a.id).then(() => fs11.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs11.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
|
|
5248
5336
|
},
|
|
5249
5337
|
limited: (at) => this.pauseForLimit(at),
|
|
5250
5338
|
usageWarning: (info) => this.paceForWarning(info),
|
|
@@ -5264,7 +5352,7 @@ Note from the manager: ${note}` : ""
|
|
|
5264
5352
|
void this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0);
|
|
5265
5353
|
if (result.interrupted) this.interrupted(a);
|
|
5266
5354
|
if (a.task === "qa") await this.onQaFinished(a, repo, result);
|
|
5267
|
-
else if (a.task === "fix") this.onFixFinished(a, repo, result);
|
|
5355
|
+
else if (a.task === "fix") await this.onFixFinished(a, repo, result);
|
|
5268
5356
|
else await this.onIssueFinished(a, repo, result);
|
|
5269
5357
|
this.emitAgent(a);
|
|
5270
5358
|
this.save();
|
|
@@ -5374,6 +5462,7 @@ Note from the manager: ${note}` : ""
|
|
|
5374
5462
|
if (rec && (rec.status === "needs-human" || rec.status === "passed" || rec.status === "failed")) {
|
|
5375
5463
|
rec.round += 1;
|
|
5376
5464
|
rec.sessionFailures = 0;
|
|
5465
|
+
rec.mergeNote = null;
|
|
5377
5466
|
}
|
|
5378
5467
|
const dev = this.state.agents.find((a) => a.repoId === repo.id && a.role === "dev" && a.task !== "qa" && (a.prNumber === prNumber || a.branch === pr.headRefName));
|
|
5379
5468
|
this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
|
|
@@ -5565,7 +5654,8 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5565
5654
|
this.setQa(rec, { status: "fixing", devAgentId: dev.id });
|
|
5566
5655
|
this.beginTask(
|
|
5567
5656
|
dev,
|
|
5568
|
-
|
|
5657
|
+
// The session they held was another task's (QA, say); the author's own is resumed below, from rec.devSessionId.
|
|
5658
|
+
{ task: "fix", issueNumber: rec.issueNumber, issueTitle: pull?.title ?? `PR #${rec.prNumber}`, branch: headRef, prNumber: rec.prNumber, prUrl: pull?.url ?? null, sessionId: null },
|
|
5569
5659
|
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}`,
|
|
5570
5660
|
`Checking out PR #${rec.prNumber}\u2026`
|
|
5571
5661
|
);
|
|
@@ -5610,8 +5700,9 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5610
5700
|
const resume = original && rec.devSessionId ? rec.devSessionId : void 0;
|
|
5611
5701
|
this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }), resume);
|
|
5612
5702
|
}
|
|
5613
|
-
onFixFinished(a, repo, result) {
|
|
5703
|
+
async onFixFinished(a, repo, result) {
|
|
5614
5704
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
5705
|
+
const head = rec && result.ok && a.status !== "stopped" && rec.fixReason !== "checks" ? await this.prHead(a, repo, rec.prNumber) : null;
|
|
5615
5706
|
if (a.status === "stopped") {
|
|
5616
5707
|
if (rec?.status === "fixing") this.setQa(rec, { status: "failed" });
|
|
5617
5708
|
return;
|
|
@@ -5622,17 +5713,40 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5622
5713
|
if (rec) this.setQa(rec, { status: failures >= 2 ? "needs-human" : "failed", sessionFailures: failures });
|
|
5623
5714
|
return;
|
|
5624
5715
|
}
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
this.appendLog(a, [{ kind: "done", text: `\u2714 PR #${a.prNumber}
|
|
5628
|
-
this.setQa(rec, { status: "passed", devSessionId: a.sessionId ?? rec.devSessionId, mergeNote: "waiting for fresh checks" });
|
|
5629
|
-
this.toast("info", `${a.name} fixed PR #${rec.prNumber}; it merges once it passes again`);
|
|
5716
|
+
if (!rec || !this.state.qa.includes(rec)) {
|
|
5717
|
+
a.status = "done";
|
|
5718
|
+
this.appendLog(a, [{ kind: "done", text: `\u2714 Fix pushed for PR #${a.prNumber} in ${this.minutes(a)}m. Back to QA.` }]);
|
|
5630
5719
|
return;
|
|
5631
5720
|
}
|
|
5632
|
-
|
|
5633
|
-
if (
|
|
5634
|
-
|
|
5635
|
-
|
|
5721
|
+
const step = fixOutcome(rec, head, this.minutes(a), !this.limited());
|
|
5722
|
+
if (!step.pushed) {
|
|
5723
|
+
const needsHuman = step.set.status === "needs-human";
|
|
5724
|
+
a.status = "error";
|
|
5725
|
+
a.lastError = `No new commits were pushed for PR #${rec.prNumber}`;
|
|
5726
|
+
this.appendLog(a, [step.log]);
|
|
5727
|
+
this.setQa(rec, step.set);
|
|
5728
|
+
this.toast("error", `${a.name} ended the fix for PR #${rec.prNumber} without pushing a commit${needsHuman ? "; it needs you" : "; it goes back for another try"}`);
|
|
5729
|
+
if (needsHuman) {
|
|
5730
|
+
this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} went back for fixes, but ${step.set.sessionFailures} fix sessions in a row ended without pushing a commit, so it needs you.`);
|
|
5731
|
+
}
|
|
5732
|
+
return;
|
|
5733
|
+
}
|
|
5734
|
+
a.status = "done";
|
|
5735
|
+
this.appendLog(a, [step.log]);
|
|
5736
|
+
this.setQa(rec, { ...step.set, devSessionId: a.sessionId ?? rec.devSessionId });
|
|
5737
|
+
this.toast(
|
|
5738
|
+
"info",
|
|
5739
|
+
step.set.status === "passed" ? `${a.name} fixed PR #${rec.prNumber}; it merges once it passes again` : `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`
|
|
5740
|
+
);
|
|
5741
|
+
}
|
|
5742
|
+
/** The PR's head commit now; null (with a warning in the agent's log) when GitHub can't be asked. */
|
|
5743
|
+
async prHead(a, repo, prNumber) {
|
|
5744
|
+
try {
|
|
5745
|
+
return (await this.backend.prDetails(repo.fullName, prNumber)).headSha;
|
|
5746
|
+
} catch (err) {
|
|
5747
|
+
console.warn(`could not read the head of ${repo.fullName}#${prNumber}`, err);
|
|
5748
|
+
this.appendLog(a, [{ kind: "system", text: `\u26A0 Could not check PR #${prNumber} for new commits (${oneLine(err)}); taking the fix at its word.` }]);
|
|
5749
|
+
return null;
|
|
5636
5750
|
}
|
|
5637
5751
|
}
|
|
5638
5752
|
/** A message for an agent: sent into their running session, or a follow-up that resumes it. typed: the manager typed it at their CLI's prompt, where it's already running. */
|
|
@@ -5674,7 +5788,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5674
5788
|
if (patch.ceoHeartbeatMin !== void 0) s.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
5675
5789
|
if (typeof patch.managerName === "string") s.managerName = patch.managerName.trim().slice(0, 40);
|
|
5676
5790
|
if (typeof patch.companyName === "string") s.companyName = patch.companyName.trim().slice(0, 60);
|
|
5677
|
-
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir =
|
|
5791
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir = path13.resolve(patch.projectsDir.trim());
|
|
5678
5792
|
if (typeof patch.setupDone === "boolean") s.setupDone = patch.setupDone;
|
|
5679
5793
|
if (patch.tutorialStep !== void 0) s.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
5680
5794
|
if (typeof patch.autoUpdate === "boolean") s.autoUpdate = patch.autoUpdate;
|
|
@@ -5953,13 +6067,14 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5953
6067
|
const u = this.officeUpdate;
|
|
5954
6068
|
Object.assign(u, { sent: false, handedOver: false, requested: false, drainingSince: null });
|
|
5955
6069
|
const interrupted = this.state.agents.filter((a) => BUSY.includes(a.status));
|
|
6070
|
+
const preparing = new Set(interrupted.filter((a) => a.status === "preparing"));
|
|
5956
6071
|
for (const a of interrupted) {
|
|
5957
6072
|
Object.assign(a, { status: "stopped", lastError: "The office updated itself while this agent was working." });
|
|
5958
6073
|
const rt = this.agentRt.get(a.id);
|
|
5959
6074
|
if (rt) Object.assign(rt, { session: null, currentTool: null });
|
|
5960
6075
|
}
|
|
5961
6076
|
this.ensureCeo(interrupted);
|
|
5962
|
-
this.recover(interrupted);
|
|
6077
|
+
this.recover(interrupted, preparing);
|
|
5963
6078
|
interrupted.forEach((a) => this.emitAgent(a));
|
|
5964
6079
|
await this.reportUpdate(result);
|
|
5965
6080
|
if (!result.ok) u.failedBehind = u.behind;
|
|
@@ -6120,7 +6235,7 @@ ${job.text}` } : job;
|
|
|
6120
6235
|
this.emitAgent(a);
|
|
6121
6236
|
this.emitCeo();
|
|
6122
6237
|
this.save();
|
|
6123
|
-
await
|
|
6238
|
+
await fs11.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
6124
6239
|
if (a.status !== "working") {
|
|
6125
6240
|
this.state.ceo.job = null;
|
|
6126
6241
|
this.emitCeo();
|
|
@@ -6136,7 +6251,7 @@ ${job.text}` } : job;
|
|
|
6136
6251
|
name: a.name,
|
|
6137
6252
|
company: s.companyName,
|
|
6138
6253
|
manager: s.managerName,
|
|
6139
|
-
notesFile:
|
|
6254
|
+
notesFile: path13.join(CEO_DIR, "NOTES.md"),
|
|
6140
6255
|
sessionLimit: s.sessionLimit,
|
|
6141
6256
|
teamCap: s.teamCap,
|
|
6142
6257
|
hiring: s.hiring
|
|
@@ -6363,6 +6478,12 @@ ${t}`);
|
|
|
6363
6478
|
const mark = `\u2026 (truncated, ${brief.length} chars; see agent_detail)`;
|
|
6364
6479
|
return brief.slice(0, 400 - mark.length).trimEnd() + mark;
|
|
6365
6480
|
};
|
|
6481
|
+
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6482
|
+
const seats = (r, role) => seatCount(
|
|
6483
|
+
MAX_DESKS[role],
|
|
6484
|
+
this.state.agents.filter((a) => a.repoId === r.id && a.role === role).length,
|
|
6485
|
+
pending2.filter((p) => p.kind === "hire" && p.repoId === r.id && p.role === role).length
|
|
6486
|
+
);
|
|
6366
6487
|
const floors = [...this.state.repos].sort((x, y) => x.floor - y.floor).map((r) => {
|
|
6367
6488
|
const rt = this.repoRt.get(r.id);
|
|
6368
6489
|
const open = new Set(rt.issues.map((i) => i.number));
|
|
@@ -6378,6 +6499,7 @@ ${t}`);
|
|
|
6378
6499
|
autoAssign: r.autoAssign,
|
|
6379
6500
|
autoMerge: r.autoMerge,
|
|
6380
6501
|
folderSync: rt.folderSync,
|
|
6502
|
+
seats: { dev: seats(r, "dev"), qa: seats(r, "qa") },
|
|
6381
6503
|
capacity: {
|
|
6382
6504
|
developers: this.state.agents.filter((a) => a.repoId === r.id && a.role === "dev").length,
|
|
6383
6505
|
developersFree: this.available(r, "dev").length,
|
|
@@ -6431,10 +6553,11 @@ ${t}`);
|
|
|
6431
6553
|
sessionLimit: s.sessionLimit || "none",
|
|
6432
6554
|
sessionsRunning: this.running(),
|
|
6433
6555
|
usage: usageLabel(this.usageNow(), Date.now()),
|
|
6434
|
-
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa }
|
|
6556
|
+
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa },
|
|
6557
|
+
proposalLimit: { pending: pending2.length, max: MAX_PENDING_PROPOSALS }
|
|
6435
6558
|
},
|
|
6436
6559
|
floors,
|
|
6437
|
-
pendingProposals:
|
|
6560
|
+
pendingProposals: pending2.map(req),
|
|
6438
6561
|
recentDecisions: this.state.requests.filter((r) => r.status !== "pending").slice(-10).map(req)
|
|
6439
6562
|
},
|
|
6440
6563
|
null,
|
|
@@ -6493,7 +6616,7 @@ ${t}`);
|
|
|
6493
6616
|
if (!title) throw new Error("A hire needs a job title.");
|
|
6494
6617
|
const specialty = specialtySlug(x.specialty);
|
|
6495
6618
|
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6496
|
-
|
|
6619
|
+
checkPendingLimit(pending2.length);
|
|
6497
6620
|
const dup = pending2.find((r) => r.kind === "hire" && r.repoId === repo.id && r.role === role && r.specialty === specialty);
|
|
6498
6621
|
if (dup) throw new Error(`${dup.name} (${dup.title}) is already proposed for floor ${repo.floor} with that specialty.`);
|
|
6499
6622
|
const seated = this.state.agents.filter((a) => a.repoId === repo.id && a.role === role).length;
|
|
@@ -6541,7 +6664,7 @@ ${t}`);
|
|
|
6541
6664
|
}
|
|
6542
6665
|
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6543
6666
|
if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
|
|
6544
|
-
|
|
6667
|
+
checkPendingLimit(pending2.length);
|
|
6545
6668
|
const req = {
|
|
6546
6669
|
id: crypto6.randomUUID(),
|
|
6547
6670
|
kind: "let-go",
|
|
@@ -6742,10 +6865,10 @@ app.post(
|
|
|
6742
6865
|
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 }))
|
|
6743
6866
|
);
|
|
6744
6867
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
6745
|
-
var dist =
|
|
6746
|
-
if (
|
|
6868
|
+
var dist = path14.resolve(import.meta.dirname, "../dist");
|
|
6869
|
+
if (fs12.existsSync(dist)) {
|
|
6747
6870
|
app.use(express.static(dist));
|
|
6748
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
6871
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path14.join(dist, "index.html")));
|
|
6749
6872
|
}
|
|
6750
6873
|
app.use((err, _req, res, _next) => {
|
|
6751
6874
|
const parserStatus = err?.status;
|