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.
@@ -1,7 +1,7 @@
1
1
  // server/index.ts
2
2
  import http2 from "node:http";
3
- import path13 from "node:path";
4
- import fs11 from "node:fs";
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
- import { query } from "@anthropic-ai/claude-agent-sdk";
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: fs3.readFileSync(path3.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
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 = fs3.readdirSync(dir);
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[path3.extname(name).toLowerCase()];
590
- const file = path3.join(dir, name);
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 - fs3.statSync(file).mtimeMs < 1e3) continue;
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 = path3.relative(cwd, path3.resolve(cwd, s));
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 args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--init-script", path3.join(import.meta.dirname, "browser-init.js")];
733
- mcpServers.playwright = process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
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 fs6 from "node:fs";
907
- import path7 from "node:path";
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 fs4 from "node:fs";
913
- import { createRequire } from "node:module";
914
- import path4 from "node:path";
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(path4.delimiter).filter(Boolean);
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 = path4.join(dir, name + ext.toLowerCase());
968
+ const file = path5.join(dir, name + ext.toLowerCase());
930
969
  try {
931
- const st = fs4.statSync(file);
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 = path4.dirname(shimPath);
943
- const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path4.join(dir, m[1]));
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 = path4.join(dir, "node.exe");
951
- return { file: fs4.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
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 = createRequire(import.meta.url).resolve("@anthropic-ai/claude-agent-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 = createRequire(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
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, fs4.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
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 fs5 from "node:fs";
1225
+ import fs6 from "node:fs";
1187
1226
  import net from "node:net";
1188
- import path6 from "node:path";
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 path5 from "node:path";
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 path5.join(home, "pty.sock");
1197
- const tag = crypto.createHash("sha1").update(path5.resolve(home).toLowerCase()).digest("hex").slice(0, 12);
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 = path6.join(import.meta.dirname, `ptyHost${path6.extname(import.meta.filename)}`);
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 = path6.join(home, "pty.secret");
1375
+ const secretFile = path7.join(home, "pty.secret");
1337
1376
  try {
1338
- if (!fs5.existsSync(secretFile)) {
1339
- fs5.mkdirSync(home, { recursive: true });
1340
- fs5.writeFileSync(secretFile, crypto2.randomBytes(24).toString("hex"), { mode: 384 });
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 = fs5.readFileSync(secretFile, "utf8").trim();
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 = path7.join(HOME_DIR, "sessions");
1427
- var BIN_DIR = path7.join(HOME_DIR, "bin");
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
- fs6.mkdirSync(BIN_DIR, { recursive: true });
1466
- fs6.writeFileSync(path7.join(BIN_DIR, "statusline.cjs"), STATUSLINE_SOURCE);
1467
- fs6.writeFileSync(path7.join(BIN_DIR, "notify.cjs"), NOTIFY_SOURCE);
1468
- fs6.writeFileSync(path7.join(BIN_DIR, "codex-hook.cjs"), CODEX_HOOK_SOURCE);
1469
- fs6.writeFileSync(path7.join(BIN_DIR, "opencode-plugin.mjs"), OPENCODE_PLUGIN_SOURCE);
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
- fs6.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
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 = path7.join(SESSIONS_DIR, token);
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(path7.join(l2.dir, "browser"), l2.shots, all ? Infinity : Date.now())) {
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(fs6.readFileSync(shot.file), shot.mime);
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, fs6.existsSync);
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 ? playwrightServer(path7.join(dir, "browser")) : null;
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
- fs6.mkdirSync(dir, { recursive: true });
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(path7.join(BIN_DIR, "statusline.cjs"))} ${shellArg(hookUrl)}`, padding: 0 }
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: path7.join(dir, "settings.json"), mcp: Object.keys(servers).length ? path7.join(dir, "mcp.json") : null, system: path7.join(dir, "instructions.md") };
1882
- fs6.writeFileSync(files.settings, JSON.stringify(settings, null, 2));
1883
- if (files.mcp) fs6.writeFileSync(files.mcp, JSON.stringify({ mcpServers: servers }, null, 2));
1884
- fs6.writeFileSync(files.system, opts.systemAppend);
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 = path7.join(dir, "task.md");
1887
- fs6.writeFileSync(file, prompt);
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: path7.join(BIN_DIR, "notify.cjs"), url: hookUrl },
1908
- codexHook: path7.join(BIN_DIR, "codex-hook.cjs"),
1909
- plugin: pathToFileURL(path7.join(BIN_DIR, "opencode-plugin.mjs")).href,
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 fs7 from "node:fs/promises";
2012
+ import fs8 from "node:fs/promises";
1976
2013
  import net2 from "node:net";
1977
- import path8 from "node:path";
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 fs7.readFile(file, "utf8"));
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 = path8.join(wt, PREVIEW_TMP);
2075
- await fs7.mkdir(tmp, { recursive: true });
2076
- const pkg = await readJson(path8.join(wt, "package.json"));
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 = path8.join(wt, "package-lock.json");
2116
- const lock = await fs7.readFile(lockFile, "utf8").catch(() => null);
2117
- const hash = crypto4.createHash("sha256").update(await fs7.readFile(path8.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
2118
- const marker = path8.join(wt, "node_modules", INSTALL_MARKER);
2119
- if (await fs7.readFile(marker, "utf8").catch(() => "") === hash) {
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 fs7.writeFile(marker, hash).catch(() => void 0);
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) => fs7.access(path8.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
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 fs8 from "node:fs/promises";
2169
- import path9 from "node:path";
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 = path9.join(home, "last-update.json");
2207
- const raw = await fs8.readFile(file, "utf8").catch(() => null);
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 fs8.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
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 = path9.resolve(import.meta.dirname, "..");
2218
- var isSamePath = (a, b) => process.platform === "win32" ? path9.resolve(a).toLowerCase() === path9.resolve(b).toLowerCase() : path9.resolve(a) === path9.resolve(b);
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 path10 from "node:path";
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 = path10.basename(opts.cwd);
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}-${path10.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
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
- cb.log([{ kind: "result", text: ` \u23BF ${s.floors.length} floors \xB7 ${people} people \xB7 ${issues} open issues \xB7 ${s.pendingProposals.length} proposals pending` }]);
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 fs10 from "node:fs/promises";
3106
- import path12 from "node:path";
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 fs9 from "node:fs/promises";
3733
- import path11 from "node:path";
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 fs9.mkdir(path11.dirname(file), { recursive: true });
3828
- await fs9.writeFile(`${file}.tmp`, this.snapshot());
3829
- await fs9.rename(`${file}.tmp`, file);
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 fs9.readFile(file, "utf8").catch(() => "");
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 = { dev: 12, qa: 3, ceo: 1 };
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 = path12.join(HOME_DIR, "ceo");
3970
- var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path12.resolve(import.meta.dirname, ".."));
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 = path12.join(HOME_DIR, "screens");
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) => path12.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
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 = path12.join(SCREENS_DIR, `${agentId}.${ext}`);
4073
+ const file = path13.join(SCREENS_DIR, `${agentId}.${ext}`);
3989
4074
  try {
3990
- const [data, stat] = await Promise.all([fs10.readFile(file), fs10.stat(file)]);
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) => fs10.rm(path12.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
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 = path12.join(HOME_DIR, "terminals");
4001
- var terminalFile = (agentId) => path12.join(TERMINALS_DIR, `${agentId}.ansi`);
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 fs10.readFile(STATE_FILE, "utf8");
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.some((c) => c.agentId === a.id && c.busy)) {
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
- /** Agents cut off by a server restart pick their Claude Code session back up (QA and demo agents start over). */
4287
- recover(agents) {
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
- if (a.task === "qa" || this.backend.demo || !a.sessionId || !a.branch) {
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, "The office server restarted while you were working. Check the state of your worktree and continue where you left off.").catch(
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 fs10.mkdir(path12.dirname(STATE_FILE), { recursive: true });
4561
+ await fs11.mkdir(path13.dirname(STATE_FILE), { recursive: true });
4474
4562
  const tmp = `${STATE_FILE}.tmp`;
4475
- await fs10.writeFile(tmp, JSON.stringify(this.state, null, 2));
4476
- await fs10.rename(tmp, STATE_FILE);
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 path12.join(root, name);
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() ? path12.resolve(dir.trim()) : this.state.settings.projectsDir;
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) => path12.resolve(a).toLowerCase() === path12.resolve(b).toLowerCase();
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 ? path12.basename(repo.localPath) : repo.fullName;
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 fs10.rm(terminalFile(id), { force: true }).catch(() => void 0);
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 fs10.stat(terminalFile(agentId)).catch(() => null)) return null;
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(() => fs10.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs10.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
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
- { task: "fix", issueNumber: rec.issueNumber, issueTitle: pull?.title ?? `PR #${rec.prNumber}`, branch: headRef, prNumber: rec.prNumber, prUrl: pull?.url ?? null },
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
- a.status = "done";
5626
- if (rec && (rec.fixReason === "checks" || rec.fixReason === "conflict")) {
5627
- this.appendLog(a, [{ kind: "done", text: `\u2714 PR #${a.prNumber} fixed in ${this.minutes(a)}m. Back in line to merge.` }]);
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
- this.appendLog(a, [{ kind: "done", text: `\u2714 Fix pushed for PR #${a.prNumber} in ${this.minutes(a)}m. Back to QA.` }]);
5633
- if (rec) {
5634
- this.setQa(rec, { status: "queued", round: rec.round + 1, devSessionId: a.sessionId ?? rec.devSessionId });
5635
- this.toast("info", `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`);
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 = path12.resolve(patch.projectsDir.trim());
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 fs10.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
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: path12.join(CEO_DIR, "NOTES.md"),
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: this.state.requests.filter((r) => r.status === "pending").map(req),
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
- if (pending2.length >= MAX_PENDING_REQUESTS) throw new Error(`${pending2.length} proposals are already waiting for the manager. Wait for their decisions first.`);
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
- if (pending2.length >= MAX_PENDING_REQUESTS) throw new Error(`${pending2.length} proposals are already waiting for the manager. Wait for their decisions first.`);
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 = path13.resolve(import.meta.dirname, "../dist");
6746
- if (fs11.existsSync(dist)) {
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(path13.join(dist, "index.html")));
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;