cubefarm 0.3.6 → 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;
@@ -2465,7 +2502,7 @@ function fakeSession(opts, cb, fullName) {
2465
2502
  number: n,
2466
2503
  title,
2467
2504
  url,
2468
- 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}$/, "")}`,
2469
2506
  state: "OPEN",
2470
2507
  isDraft: false,
2471
2508
  mergeable: "MERGEABLE",
@@ -2912,7 +2949,8 @@ function ceoSession(opts, cb) {
2912
2949
  const s = JSON.parse(await office.call("company_status", {}));
2913
2950
  const people = s.floors.reduce((n, f) => n + f.team.length, 0);
2914
2951
  const issues = s.floors.reduce((n, f) => n + f.backlog.length, 0);
2915
- 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` }]);
2916
2954
  return s;
2917
2955
  };
2918
2956
  const use = async (name, args) => {
@@ -3103,8 +3141,8 @@ Acceptance criteria:
3103
3141
  }
3104
3142
 
3105
3143
  // server/swarm.ts
3106
- import fs10 from "node:fs/promises";
3107
- import path12 from "node:path";
3144
+ import fs11 from "node:fs/promises";
3145
+ import path13 from "node:path";
3108
3146
  import crypto6 from "node:crypto";
3109
3147
 
3110
3148
  // server/ceo.ts
@@ -3189,7 +3227,7 @@ function createOfficeTools(h) {
3189
3227
  const defs = [
3190
3228
  tool(
3191
3229
  "company_status",
3192
- "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.",
3193
3231
  {},
3194
3232
  () => run2(() => h.companyStatus())
3195
3233
  ),
@@ -3281,6 +3319,12 @@ function createOfficeTools(h) {
3281
3319
  }
3282
3320
  };
3283
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
+ }
3284
3328
  function ceoSystemPrompt(o) {
3285
3329
  const manager = o.manager ? `the manager, ${o.manager}` : "the human manager";
3286
3330
  return [
@@ -3289,7 +3333,7 @@ function ceoSystemPrompt(o) {
3289
3333
  "",
3290
3334
  "Your job is to run the company, not to write code:",
3291
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.",
3292
- `- 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.`,
3293
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.",
3294
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).",
3295
3339
  "",
@@ -3771,8 +3815,8 @@ function pacingMessage(w, until, sessions, now2) {
3771
3815
  }
3772
3816
 
3773
3817
  // server/terminal.ts
3774
- import fs9 from "node:fs/promises";
3775
- import path11 from "node:path";
3818
+ import fs10 from "node:fs/promises";
3819
+ import path12 from "node:path";
3776
3820
  import xtermHeadless from "@xterm/headless";
3777
3821
  import xtermSerialize from "@xterm/addon-serialize";
3778
3822
  var { Terminal } = xtermHeadless;
@@ -3866,12 +3910,12 @@ var AgentTerminal = class {
3866
3910
  async save(file) {
3867
3911
  this.dirty = false;
3868
3912
  await this.flush();
3869
- await fs9.mkdir(path11.dirname(file), { recursive: true });
3870
- await fs9.writeFile(`${file}.tmp`, this.snapshot());
3871
- 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);
3872
3916
  }
3873
3917
  async load(file) {
3874
- const data = await fs9.readFile(file, "utf8").catch(() => "");
3918
+ const data = await fs10.readFile(file, "utf8").catch(() => "");
3875
3919
  if (!data) return;
3876
3920
  this.term.write(data);
3877
3921
  this.note("(the office restarted)");
@@ -4001,16 +4045,15 @@ var FEMININE_NAMES = new Set(
4001
4045
  );
4002
4046
  var lookFor = (name) => FEMININE_NAMES.has(name.trim().split(/\s+/)[0].toLowerCase()) ? "feminine" : "masculine";
4003
4047
  var LOOKS = ["feminine", "masculine"];
4004
- var MAX_DESKS = { dev: 12, qa: 3, ceo: 1 };
4048
+ var MAX_DESKS = { ...FLOOR_DESKS, ceo: 1 };
4005
4049
  var MAX_QA_ROUNDS = 3;
4006
4050
  var DEFAULT_MODEL = "claude-opus-5-5";
4007
4051
  var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
4008
4052
  var CEO_MODEL = "claude-opus-5-5";
4009
4053
  var CEO_EFFORT = "xhigh";
4010
4054
  var CEO_NAME = "Morgan";
4011
- var CEO_DIR = path12.join(HOME_DIR, "ceo");
4012
- var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path12.resolve(import.meta.dirname, ".."));
4013
- 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, ".."));
4014
4057
  var MAX_ISSUES_PER_JOB = 12;
4015
4058
  var KEEP_MESSAGES = 200;
4016
4059
  var KEEP_DECIDED_REQUESTS = 40;
@@ -4022,14 +4065,14 @@ var ERROR_COOLDOWN_MS = 2 * 6e4;
4022
4065
  var MAX_ISSUE_FAILURES = 2;
4023
4066
  var LIMIT_PAUSE_MS = 15 * 6e4;
4024
4067
  var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
4025
- var SCREENS_DIR = path12.join(HOME_DIR, "screens");
4068
+ var SCREENS_DIR = path13.join(HOME_DIR, "screens");
4026
4069
  var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
4027
- 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"}`);
4028
4071
  async function loadScreen(agentId) {
4029
4072
  for (const [mime, ext] of Object.entries(MIME_EXT)) {
4030
- const file = path12.join(SCREENS_DIR, `${agentId}.${ext}`);
4073
+ const file = path13.join(SCREENS_DIR, `${agentId}.${ext}`);
4031
4074
  try {
4032
- 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)]);
4033
4076
  return { data, mime, at: stat.mtimeMs };
4034
4077
  } catch {
4035
4078
  }
@@ -4037,10 +4080,10 @@ async function loadScreen(agentId) {
4037
4080
  return null;
4038
4081
  }
4039
4082
  async function removeScreens(agentId) {
4040
- 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 })));
4041
4084
  }
4042
- var TERMINALS_DIR = path12.join(HOME_DIR, "terminals");
4043
- 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`);
4044
4087
  var TERMINAL_SAVE_MS = 2e4;
4045
4088
  var QA_SCHEMA = {
4046
4089
  type: "object",
@@ -4194,7 +4237,7 @@ var Swarm = class {
4194
4237
  // ---------- lifecycle ----------
4195
4238
  async init() {
4196
4239
  try {
4197
- const raw = await fs10.readFile(STATE_FILE, "utf8");
4240
+ const raw = await fs11.readFile(STATE_FILE, "utf8");
4198
4241
  const loaded = JSON.parse(raw);
4199
4242
  this.state = {
4200
4243
  settings: { ...this.state.settings, ...loaded.settings },
@@ -4515,10 +4558,10 @@ var Swarm = class {
4515
4558
  if (this.saveTimer) clearTimeout(this.saveTimer);
4516
4559
  this.saveTimer = null;
4517
4560
  for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
4518
- await fs10.mkdir(path12.dirname(STATE_FILE), { recursive: true });
4561
+ await fs11.mkdir(path13.dirname(STATE_FILE), { recursive: true });
4519
4562
  const tmp = `${STATE_FILE}.tmp`;
4520
- await fs10.writeFile(tmp, JSON.stringify(this.state, null, 2));
4521
- await fs10.rename(tmp, STATE_FILE);
4563
+ await fs11.writeFile(tmp, JSON.stringify(this.state, null, 2));
4564
+ await fs11.rename(tmp, STATE_FILE);
4522
4565
  }
4523
4566
  // ---------- lookups ----------
4524
4567
  repo(id) {
@@ -4600,7 +4643,7 @@ var Swarm = class {
4600
4643
  const name = fullName.split("/")[1];
4601
4644
  const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
4602
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.`);
4603
- return path12.join(root, name);
4646
+ return path13.join(root, name);
4604
4647
  }
4605
4648
  /** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
4606
4649
  async createRepo(name, opts) {
@@ -4612,11 +4655,11 @@ var Swarm = class {
4612
4655
  }
4613
4656
  /** The folders in your projects folder (or another folder you point at), and which are floors already. */
4614
4657
  async listProjectFolders(dir) {
4615
- 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;
4616
4659
  const folders = await this.backend.scanProjects(root).catch((err) => {
4617
4660
  throw new HttpError(400, err.message);
4618
4661
  });
4619
- 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();
4620
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;
4621
4664
  return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
4622
4665
  }
@@ -4796,7 +4839,7 @@ var Swarm = class {
4796
4839
  rt.folderSync = status;
4797
4840
  this.emitRepo(repo);
4798
4841
  if (status && status !== before) {
4799
- const name = repo.localPath ? path12.basename(repo.localPath) : repo.fullName;
4842
+ const name = repo.localPath ? path13.basename(repo.localPath) : repo.fullName;
4800
4843
  if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
4801
4844
  else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
4802
4845
  }
@@ -5020,7 +5063,7 @@ var Swarm = class {
5020
5063
  this.agentRt.get(id)?.terminal?.releaseIdle?.();
5021
5064
  this.agentRt.get(id)?.terminal?.dispose();
5022
5065
  void removeScreens(id);
5023
- void fs10.rm(terminalFile(id), { force: true }).catch(() => void 0);
5066
+ void fs11.rm(terminalFile(id), { force: true }).catch(() => void 0);
5024
5067
  for (const q of this.state.qa) {
5025
5068
  if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
5026
5069
  if (q.devAgentId === id) q.devAgentId = null;
@@ -5123,7 +5166,7 @@ var Swarm = class {
5123
5166
  }
5124
5167
  // ---------- terminals ----------
5125
5168
  async loadTerminal(agentId) {
5126
- if (!await fs10.stat(terminalFile(agentId)).catch(() => null)) return null;
5169
+ if (!await fs11.stat(terminalFile(agentId)).catch(() => null)) return null;
5127
5170
  const t = this.newTerminal(agentId);
5128
5171
  await t.load(terminalFile(agentId));
5129
5172
  return t;
@@ -5289,7 +5332,7 @@ Note from the manager: ${note}` : ""
5289
5332
  rt.shots.push({ data, mime, url: rt.browserUrl, at });
5290
5333
  if (rt.shots.length > 12) rt.shots.shift();
5291
5334
  this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
5292
- 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));
5293
5336
  },
5294
5337
  limited: (at) => this.pauseForLimit(at),
5295
5338
  usageWarning: (info) => this.paceForWarning(info),
@@ -5745,7 +5788,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
5745
5788
  if (patch.ceoHeartbeatMin !== void 0) s.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
5746
5789
  if (typeof patch.managerName === "string") s.managerName = patch.managerName.trim().slice(0, 40);
5747
5790
  if (typeof patch.companyName === "string") s.companyName = patch.companyName.trim().slice(0, 60);
5748
- 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());
5749
5792
  if (typeof patch.setupDone === "boolean") s.setupDone = patch.setupDone;
5750
5793
  if (patch.tutorialStep !== void 0) s.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
5751
5794
  if (typeof patch.autoUpdate === "boolean") s.autoUpdate = patch.autoUpdate;
@@ -6192,7 +6235,7 @@ ${job.text}` } : job;
6192
6235
  this.emitAgent(a);
6193
6236
  this.emitCeo();
6194
6237
  this.save();
6195
- await fs10.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
6238
+ await fs11.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
6196
6239
  if (a.status !== "working") {
6197
6240
  this.state.ceo.job = null;
6198
6241
  this.emitCeo();
@@ -6208,7 +6251,7 @@ ${job.text}` } : job;
6208
6251
  name: a.name,
6209
6252
  company: s.companyName,
6210
6253
  manager: s.managerName,
6211
- notesFile: path12.join(CEO_DIR, "NOTES.md"),
6254
+ notesFile: path13.join(CEO_DIR, "NOTES.md"),
6212
6255
  sessionLimit: s.sessionLimit,
6213
6256
  teamCap: s.teamCap,
6214
6257
  hiring: s.hiring
@@ -6435,6 +6478,12 @@ ${t}`);
6435
6478
  const mark = `\u2026 (truncated, ${brief.length} chars; see agent_detail)`;
6436
6479
  return brief.slice(0, 400 - mark.length).trimEnd() + mark;
6437
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
+ );
6438
6487
  const floors = [...this.state.repos].sort((x, y) => x.floor - y.floor).map((r) => {
6439
6488
  const rt = this.repoRt.get(r.id);
6440
6489
  const open = new Set(rt.issues.map((i) => i.number));
@@ -6450,6 +6499,7 @@ ${t}`);
6450
6499
  autoAssign: r.autoAssign,
6451
6500
  autoMerge: r.autoMerge,
6452
6501
  folderSync: rt.folderSync,
6502
+ seats: { dev: seats(r, "dev"), qa: seats(r, "qa") },
6453
6503
  capacity: {
6454
6504
  developers: this.state.agents.filter((a) => a.repoId === r.id && a.role === "dev").length,
6455
6505
  developersFree: this.available(r, "dev").length,
@@ -6503,10 +6553,11 @@ ${t}`);
6503
6553
  sessionLimit: s.sessionLimit || "none",
6504
6554
  sessionsRunning: this.running(),
6505
6555
  usage: usageLabel(this.usageNow(), Date.now()),
6506
- 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 }
6507
6558
  },
6508
6559
  floors,
6509
- pendingProposals: this.state.requests.filter((r) => r.status === "pending").map(req),
6560
+ pendingProposals: pending2.map(req),
6510
6561
  recentDecisions: this.state.requests.filter((r) => r.status !== "pending").slice(-10).map(req)
6511
6562
  },
6512
6563
  null,
@@ -6565,7 +6616,7 @@ ${t}`);
6565
6616
  if (!title) throw new Error("A hire needs a job title.");
6566
6617
  const specialty = specialtySlug(x.specialty);
6567
6618
  const pending2 = this.state.requests.filter((r) => r.status === "pending");
6568
- 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);
6569
6620
  const dup = pending2.find((r) => r.kind === "hire" && r.repoId === repo.id && r.role === role && r.specialty === specialty);
6570
6621
  if (dup) throw new Error(`${dup.name} (${dup.title}) is already proposed for floor ${repo.floor} with that specialty.`);
6571
6622
  const seated = this.state.agents.filter((a) => a.repoId === repo.id && a.role === role).length;
@@ -6613,7 +6664,7 @@ ${t}`);
6613
6664
  }
6614
6665
  const pending2 = this.state.requests.filter((r) => r.status === "pending");
6615
6666
  if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
6616
- 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);
6617
6668
  const req = {
6618
6669
  id: crypto6.randomUUID(),
6619
6670
  kind: "let-go",
@@ -6814,10 +6865,10 @@ app.post(
6814
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 }))
6815
6866
  );
6816
6867
  app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
6817
- var dist = path13.resolve(import.meta.dirname, "../dist");
6818
- if (fs11.existsSync(dist)) {
6868
+ var dist = path14.resolve(import.meta.dirname, "../dist");
6869
+ if (fs12.existsSync(dist)) {
6819
6870
  app.use(express.static(dist));
6820
- 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")));
6821
6872
  }
6822
6873
  app.use((err, _req, res, _next) => {
6823
6874
  const parserStatus = err?.status;