cubefarm 0.2.0 → 0.3.0

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 path8 from "node:path";
4
- import fs6 from "node:fs";
3
+ import path13 from "node:path";
4
+ import fs11 from "node:fs";
5
5
  import express from "express";
6
6
  import { WebSocketServer } from "ws";
7
7
 
@@ -474,20 +474,38 @@ function removeDesk(fullName, agentSlug) {
474
474
  });
475
475
  }
476
476
  var LAUNCHERS = ["node.exe", "cmd.exe", "bash.exe", "sh.exe", "conhost.exe", "python.exe", "npm.exe", "npx.exe", "bun.exe", "deno.exe"];
477
- async function releaseDesk(fullName, agentSlug, port) {
477
+ var pathForms = (p) => [.../* @__PURE__ */ new Set([p, p.replaceAll("\\", "/"), p.replaceAll("\\", "\\\\")])];
478
+ function leftoversInDesk(listing, desk, keep) {
479
+ const out = [];
480
+ for (const line of listing.split("\n")) {
481
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
482
+ if (!m) continue;
483
+ const [pid, args] = [Number(m[1]), m[2]];
484
+ if (keep.pids.includes(pid) || keep.markers.some((mark) => pathForms(mark).some((f) => args.includes(f)))) continue;
485
+ if (pathForms(desk).some((f) => args.includes(f))) out.push(pid);
486
+ }
487
+ return out;
488
+ }
489
+ async function releaseDesk(fullName, agentSlug, port, keep = { pids: [], markers: [] }) {
478
490
  const desk = deskDir(fullName, agentSlug);
491
+ const spare = [process.pid, ...keep.pids];
479
492
  if (process.platform === "win32") {
480
- const variants = [desk, desk.replaceAll("\\", "/")].map((d) => `'${d.toLowerCase().replaceAll("'", "''")}'`).join(", ");
493
+ const psList = (paths) => paths.flatMap(pathForms).map((d) => `'${d.toLowerCase().replaceAll("'", "''")}'`).join(", ");
481
494
  const script = `
482
495
  $ErrorActionPreference = 'SilentlyContinue'
496
+ $spare = @(${spare.join(", ")})
483
497
  $seed = @()
484
498
  Get-NetTCPConnection -LocalPort ${port} -State Listen | ForEach-Object { $seed += [int]$_.OwningProcess }
485
- $desks = @(${variants})
499
+ $desks = @(${psList([desk])})
500
+ $marks = @(${psList(keep.markers)})
486
501
  $all = Get-CimInstance Win32_Process
487
502
  $byId = @{}; foreach ($p in $all) { $byId[[int]$p.ProcessId] = $p }
488
503
  foreach ($p in $all) {
489
504
  if (-not $p.CommandLine) { continue }
490
505
  $cmd = $p.CommandLine.ToLower()
506
+ $ours = $false
507
+ foreach ($m in $marks) { if ($cmd.Contains($m)) { $ours = $true } }
508
+ if ($ours) { continue }
491
509
  foreach ($d in $desks) { if ($cmd.Contains($d)) { $seed += [int]$p.ProcessId } }
492
510
  }
493
511
  $launchers = @(${LAUNCHERS.map((n) => `'${n}'`).join(", ")})
@@ -495,7 +513,7 @@ $kill = New-Object 'System.Collections.Generic.HashSet[int]'
495
513
  foreach ($id in $seed) {
496
514
  $cur = $byId[$id]
497
515
  $first = $true
498
- while ($cur -and [int]$cur.ProcessId -ne ${process.pid} -and ($first -or $launchers -contains $cur.Name.ToLower())) {
516
+ while ($cur -and -not ($spare -contains [int]$cur.ProcessId) -and ($first -or $launchers -contains $cur.Name.ToLower())) {
499
517
  [void]$kill.Add([int]$cur.ProcessId)
500
518
  $first = $false
501
519
  $cur = $byId[[int]$cur.ParentProcessId]
@@ -506,8 +524,11 @@ Write-Output $kill.Count`;
506
524
  await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { timeoutMs: 3e4 }).catch(() => void 0);
507
525
  return;
508
526
  }
509
- const pids = await run("sh", ["-c", `lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null; pgrep -f ${JSON.stringify(desk)} 2>/dev/null`]).catch(() => "");
510
- for (const pid of pids.split(/\s+/).map(Number).filter((p) => p && p !== process.pid)) {
527
+ const listening = await run("sh", ["-c", `lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`]).catch(() => "");
528
+ const listing = await run("ps", ["-A", "-ww", "-o", "pid=,args="]).catch(() => "");
529
+ const pids = /* @__PURE__ */ new Set([...listening.split(/\s+/).map(Number), ...leftoversInDesk(listing, desk, keep)]);
530
+ for (const pid of pids) {
531
+ if (!pid || spare.includes(pid)) continue;
511
532
  try {
512
533
  process.kill(pid, "SIGKILL");
513
534
  } catch {
@@ -516,6 +537,7 @@ Write-Output $kill.Count`;
516
537
  }
517
538
 
518
539
  // server/agentRunner.ts
540
+ import fs3 from "node:fs";
519
541
  import path3 from "node:path";
520
542
  import { query } from "@anthropic-ai/claude-agent-sdk";
521
543
  var InputQueue = class {
@@ -544,48 +566,37 @@ var InputQueue = class {
544
566
  };
545
567
  }
546
568
  };
547
- var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
548
- var BLOCKED_COMMANDS = [
549
- { re: /\bgit\s+push\b[^\n]*(\s--force\b|\s-f\b|\s--force-with-lease\b|\s\+\S)/, why: "force-pushing is not allowed" },
550
- { re: /\bgh\s+pr\s+merge\b/, why: "the office merges pull requests once QA and the checks pass, not agents" },
551
- { re: /\bgh\s+(repo\s+(delete|edit|rename|archive)|secret|auth|release\s+delete|api\s+-X\s*DELETE)\b/, why: "repository administration is off-limits" },
552
- { re: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+(\/|~|[A-Za-z]:[\\/]?)(\s|$)/, why: "deleting a filesystem root is not allowed" },
553
- { re: /\b(shutdown|format\s+[a-z]:|mkfs|Remove-Item\s+[^\n]*-Recurse[^\n]*[A-Za-z]:\\\s*$)/i, why: "destructive system command" },
554
- { re: /\bgit\s+worktree\b/, why: "worktrees are managed by the swarm" }
555
- ];
556
- function isInside(child, parent) {
557
- const rel2 = path3.relative(parent, child);
558
- return rel2 === "" || !rel2.startsWith("..") && !path3.isAbsolute(rel2);
569
+ var allowAll = async (_toolName, input) => ({ behavior: "allow", updatedInput: input });
570
+ function screenshotFile(cwd, text) {
571
+ const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
572
+ if (!file) return null;
573
+ try {
574
+ return { data: fs3.readFileSync(path3.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
575
+ } catch {
576
+ return null;
577
+ }
559
578
  }
560
- var QA_BLOCKED = [
561
- { re: /\bgit\s+push\b/, why: "QA testers do not push code" },
562
- { re: /\bgh\s+(pr|issue)\s+(merge|close|reopen|review|comment|edit|ready|create)\b/, why: "QA testers report back to the office instead of changing PRs or issues" }
563
- ];
564
- function guardedCanUseTool(cwd, defaultBranchPush, role) {
565
- return async (toolName, input, { blockedPath }) => {
566
- if (toolName.startsWith("mcp__") && !toolName.startsWith("mcp__playwright__") && !(role === "ceo" && toolName.startsWith("mcp__office__"))) {
567
- return { behavior: "deny", message: "Only the Playwright browser tools are available to swarm agents." };
568
- }
569
- if (WRITE_TOOLS.has(toolName)) {
570
- const target = String(input.file_path ?? input.notebook_path ?? "");
571
- if (target && !isInside(path3.resolve(cwd, target), cwd)) {
572
- return { behavior: "deny", message: `You may only edit files inside your worktree (${cwd}).` };
573
- }
574
- }
575
- if (toolName === "Bash" || toolName === "PowerShell") {
576
- const cmd = String(input.command ?? "");
577
- for (const rule of role === "qa" ? [...BLOCKED_COMMANDS, ...QA_BLOCKED] : BLOCKED_COMMANDS) {
578
- if (rule.re.test(cmd)) return { behavior: "deny", message: `Blocked by cubefarm: ${rule.why}.` };
579
- }
580
- if (defaultBranchPush.test(cmd)) {
581
- return { behavior: "deny", message: "Push your own branch and open a PR; pushing to the default branch is not allowed." };
582
- }
583
- if (blockedPath && /\b(rm|del|rmdir|mv|move|Remove-Item)\b/i.test(cmd) && !isInside(path3.resolve(blockedPath), cwd)) {
584
- return { behavior: "deny", message: `That command touches ${blockedPath}, which is outside your worktree.` };
585
- }
579
+ var IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg" };
580
+ function newScreenshots(dir, seen, now2 = Date.now()) {
581
+ let names;
582
+ try {
583
+ names = fs3.readdirSync(dir);
584
+ } catch {
585
+ return [];
586
+ }
587
+ const found2 = [];
588
+ for (const name of names.sort()) {
589
+ const mime = IMAGE_MIME[path3.extname(name).toLowerCase()];
590
+ const file = path3.join(dir, name);
591
+ if (!mime || seen.has(name)) continue;
592
+ try {
593
+ if (now2 - fs3.statSync(file).mtimeMs < 1e3) continue;
594
+ } catch {
595
+ continue;
586
596
  }
587
- return { behavior: "allow", updatedInput: input };
588
- };
597
+ found2.push({ name, file, mime });
598
+ }
599
+ return found2;
589
600
  }
590
601
  var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
591
602
  function rel(cwd, p) {
@@ -628,6 +639,12 @@ function describeTool(cwd, name, input) {
628
639
  return `Subagent: ${input.description ?? ""}`;
629
640
  case "TodoWrite":
630
641
  return "Update todo list";
642
+ case "ToolSearch":
643
+ return `Load tools ${String(input.query ?? "").replace(/^select:/, "")}`;
644
+ case "apply_patch": {
645
+ const files = [...String(input.command ?? "").matchAll(/^\*\*\* (Add|Update|Delete) File: (.+)$/gm)].map((m) => `${m[1] === "Update" ? "" : `${m[1].toLowerCase()} `}${rel(cwd, m[2].trim())}`);
646
+ return `Edit ${clip(files.join(", ") || "files", 200)}`;
647
+ }
631
648
  }
632
649
  if (name.startsWith("mcp__playwright__")) {
633
650
  const action = name.replace("mcp__playwright__browser_", "").replace("mcp__playwright__", "");
@@ -676,6 +693,7 @@ function resultText(content) {
676
693
  var lineCount = (text) => text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "").length;
677
694
  function summariseResult(cwd, tool2, text) {
678
695
  if (tool2 === "Edit" || tool2 === "MultiEdit" || tool2 === "NotebookEdit") return [{ kind: "result", text: " \u23BF Updated" }];
696
+ if (tool2 === "apply_patch" && /Success\. Updated the following files/.test(text)) return [{ kind: "result", text: " \u23BF Updated" }];
679
697
  if (tool2 === "Write") return [{ kind: "result", text: " \u23BF Saved" }];
680
698
  if (tool2 === "Read") return [{ kind: "result", text: ` \u23BF Read ${lineCount(text)} lines` }];
681
699
  if (tool2 === "Glob") return [{ kind: "result", text: ` \u23BF Found ${lineCount(text)} files` }];
@@ -685,7 +703,7 @@ function summariseResult(cwd, tool2, text) {
685
703
  if (lines.length > 3) shown.push({ kind: "result", text: ` \u2026 +${lines.length - 3} lines` });
686
704
  return shown;
687
705
  }
688
- function startSession(opts, callbacks, defaultBranch) {
706
+ function startSession(opts, callbacks) {
689
707
  let done = false;
690
708
  const cb = {
691
709
  ...callbacks,
@@ -717,8 +735,6 @@ function startSession(opts, callbacks, defaultBranch) {
717
735
  if (opts.office) mcpServers.office = opts.office.server;
718
736
  const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"];
719
737
  if (opts.role === "ceo") disallowedTools.push("Bash", "PowerShell", "NotebookEdit");
720
- const escaped = defaultBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
721
- const defaultBranchPush = new RegExp(`\\bgit\\s+push\\b[^\\n]*\\s(HEAD:)?(refs/heads/)?${escaped}(\\s|$)`);
722
738
  const options = {
723
739
  cwd: opts.cwd,
724
740
  additionalDirectories: opts.additionalDirectories,
@@ -726,11 +742,8 @@ function startSession(opts, callbacks, defaultBranch) {
726
742
  env,
727
743
  model: opts.model || void 0,
728
744
  effort: opts.effort,
729
- settingSources: ["project", "local"],
730
- // Agents get exactly the MCP servers passed below: no claude.ai connectors (Gmail, Drive…),
731
- // no user-level servers or plugins from the host machine.
732
- strictMcpConfig: true,
733
- settings: { disableClaudeAiConnectors: true },
745
+ // The manager's own setup (settings, skills, plugins, MCP servers, connectors), plus the office's servers below.
746
+ settingSources: ["user", "project", "local"],
734
747
  systemPrompt: { type: "preset", preset: "claude_code", append: opts.systemAppend },
735
748
  mcpServers,
736
749
  disallowedTools,
@@ -739,7 +752,8 @@ function startSession(opts, callbacks, defaultBranch) {
739
752
  const line = data.trim();
740
753
  if (line && /error|fail/i.test(line)) cb.log([{ kind: "error", text: clip(line, 200) }]);
741
754
  },
742
- ...opts.permissionMode === "bypass" ? { permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true } : { permissionMode: "acceptEdits", canUseTool: guardedCanUseTool(opts.cwd, defaultBranchPush, opts.role) },
755
+ permissionMode: "acceptEdits",
756
+ canUseTool: allowAll,
743
757
  ...opts.outputSchema ? { outputFormat: { type: "json_schema", schema: opts.outputSchema } } : {}
744
758
  };
745
759
  let emptyTurn;
@@ -771,7 +785,7 @@ function startSession(opts, callbacks, defaultBranch) {
771
785
  cb.sessionId(msg.session_id);
772
786
  const mcp = msg.mcp_servers.map((s) => `${s.name}:${s.status}`).join(", ");
773
787
  cb.log([
774
- { kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort \xB7 ${opts.permissionMode}` },
788
+ { kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort` },
775
789
  { kind: "system", text: ` cwd ${msg.cwd}${mcp ? ` \xB7 mcp ${mcp}` : ""}` }
776
790
  ]);
777
791
  } else if (msg.subtype === "api_retry") {
@@ -805,12 +819,12 @@ function startSession(opts, callbacks, defaultBranch) {
805
819
  } else if (block.type === "thinking") {
806
820
  out.push({ kind: "thinking", text: "\u273B Thinking\u2026" });
807
821
  } else if (block.type === "tool_use") {
808
- const toolInput = block.input ?? {};
822
+ const toolInput2 = block.input ?? {};
809
823
  toolNames.set(block.id, block.name);
810
- out.push({ kind: "tool", tool: block.name, text: `\u23FA ${describeTool(opts.cwd, block.name, toolInput)}` });
811
- if (block.name === "TodoWrite") out.push(...todoLines(toolInput));
824
+ out.push({ kind: "tool", tool: block.name, text: `\u23FA ${describeTool(opts.cwd, block.name, toolInput2)}` });
825
+ if (block.name === "TodoWrite") out.push(...todoLines(toolInput2));
812
826
  cb.tool(block.name);
813
- if (block.name === "mcp__playwright__browser_navigate" && typeof toolInput.url === "string") cb.browserUrl(toolInput.url);
827
+ if (block.name === "mcp__playwright__browser_navigate" && typeof toolInput2.url === "string") cb.browserUrl(toolInput2.url);
814
828
  }
815
829
  }
816
830
  if (out.length) cb.log(out);
@@ -825,13 +839,17 @@ function startSession(opts, callbacks, defaultBranch) {
825
839
  if (block.type !== "tool_result") continue;
826
840
  const name = toolNames.get(block.tool_use_id) ?? "";
827
841
  const text = resultText(block.content);
842
+ let images = 0;
828
843
  if (Array.isArray(block.content)) {
829
844
  for (const part of block.content) {
830
845
  if (part.type === "image" && part.source.type === "base64") {
846
+ images += 1;
831
847
  cb.screenshot(Buffer.from(part.source.data, "base64"), part.source.media_type);
832
848
  }
833
849
  }
834
850
  }
851
+ const saved = name === "mcp__playwright__browser_take_screenshot" && !images ? screenshotFile(opts.cwd, text) : null;
852
+ if (saved) cb.screenshot(saved.data, saved.mime);
835
853
  if (name.startsWith("mcp__playwright__")) {
836
854
  const url = text.match(/Page URL:\s*(\S+)/);
837
855
  if (url) cb.browserUrl(url[1]);
@@ -883,12 +901,1029 @@ function startSession(opts, callbacks, defaultBranch) {
883
901
  };
884
902
  }
885
903
 
886
- // server/previewRunner.ts
904
+ // server/cliRunner.ts
905
+ import crypto3 from "node:crypto";
906
+ import fs6 from "node:fs";
907
+ import path7 from "node:path";
908
+ import { pathToFileURL } from "node:url";
909
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
910
+
911
+ // server/clis.ts
912
+ import fs4 from "node:fs";
913
+ import { createRequire } from "node:module";
914
+ import path4 from "node:path";
915
+ var CLIS = [
916
+ { id: "claude", label: "Claude Code", command: "claude", integrated: true },
917
+ { id: "codex", label: "Codex", command: "codex", integrated: false },
918
+ { id: "opencode", label: "OpenCode", command: "opencode", integrated: false }
919
+ ];
920
+ var isCli = (v) => CLIS.some((c) => c.id === v);
921
+ var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
922
+ var WIN = process.platform === "win32";
923
+ function resolveCommand(name, env = process.env) {
924
+ const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
925
+ const dirs = (env[key] ?? "").split(path4.delimiter).filter(Boolean);
926
+ const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
927
+ for (const dir of dirs) {
928
+ for (const ext of exts) {
929
+ const file = path4.join(dir, name + ext.toLowerCase());
930
+ try {
931
+ const st = fs4.statSync(file);
932
+ if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
933
+ } catch {
934
+ }
935
+ }
936
+ }
937
+ return null;
938
+ }
939
+ function unwrapCmdShim(shimPath, text) {
940
+ const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
941
+ if (!line) return null;
942
+ const dir = path4.dirname(shimPath);
943
+ const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path4.join(dir, m[1]));
944
+ const target = targets[targets.length - 1];
945
+ if (!target) return null;
946
+ if (/\.exe$/i.test(target)) return { file: target, args: [] };
947
+ if (!/\.[cm]?js$/i.test(target)) return null;
948
+ const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
949
+ 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] };
952
+ }
953
+ var bundled;
954
+ function bundledClaude() {
955
+ if (bundled !== void 0) return bundled;
956
+ bundled = null;
957
+ let sdk;
958
+ try {
959
+ sdk = createRequire(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
960
+ } catch {
961
+ return null;
962
+ }
963
+ const { platform, arch } = process;
964
+ const report = platform === "linux" ? process.report?.getReport() : void 0;
965
+ const musl = platform === "linux" && !report?.header?.glibcVersionRuntime;
966
+ const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
967
+ for (const target of targets) {
968
+ try {
969
+ bundled = createRequire(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
970
+ return bundled;
971
+ } catch {
972
+ }
973
+ }
974
+ return null;
975
+ }
976
+ function commandFor(id) {
977
+ const bundled2 = id === "claude" ? bundledClaude() : null;
978
+ if (bundled2) return { file: bundled2, args: [] };
979
+ const def = CLIS.find((c) => c.id === id);
980
+ const found2 = def && resolveCommand(def.command);
981
+ if (!found2) return null;
982
+ if (WIN && /\.(cmd|bat)$/i.test(found2)) {
983
+ try {
984
+ return unwrapCmdShim(found2, fs4.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
985
+ } catch {
986
+ return { file: found2, args: [] };
987
+ }
988
+ }
989
+ return { file: found2, args: [] };
990
+ }
991
+ async function detectClis() {
992
+ return Promise.all(
993
+ CLIS.map(async (c) => {
994
+ const cmd = commandFor(c.id);
995
+ const version = cmd ? await run(cmd.file, [...cmd.args, "--version"], { timeoutMs: 2e4 }).catch(() => null) : null;
996
+ return { id: c.id, label: c.label, installed: !!cmd, version: version?.split(/\r?\n/)[0].trim().slice(0, 60) || null, integrated: c.integrated };
997
+ })
998
+ );
999
+ }
1000
+ var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
1001
+ var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
1002
+ function codexHookCommand(node, script, win = WIN) {
1003
+ return win ? `node "${script}"` : [node, script].map((s) => `'${s.replaceAll("'", `'"'"'`)}'`).join(" ");
1004
+ }
1005
+ function launchArgs(id, ctx) {
1006
+ switch (id) {
1007
+ case "claude": {
1008
+ const disallowed = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", ...ctx.role === "ceo" ? ["Bash", "PowerShell", "NotebookEdit"] : []];
1009
+ const args = [
1010
+ ...ctx.resumeId ? ["--resume", ctx.resumeId] : ["--session-id", ctx.sessionId],
1011
+ ...ctx.model ? ["--model", ctx.model] : [],
1012
+ ...ctx.effort ? ["--effort", ctx.effort] : [],
1013
+ // The office's PreToolUse hook answers every permission question (allow, or deny with an office rule).
1014
+ "--permission-mode",
1015
+ "acceptEdits",
1016
+ // On top of the manager's own setup (settings, skills, plugins, MCP servers): the office's hooks and servers.
1017
+ "--settings",
1018
+ ctx.files.settings,
1019
+ ...ctx.files.mcp ? ["--mcp-config", ctx.files.mcp] : [],
1020
+ "--append-system-prompt-file",
1021
+ ctx.files.system,
1022
+ "--name",
1023
+ ctx.name,
1024
+ ...ctx.additionalDirectories.flatMap((d) => ["--add-dir", d]),
1025
+ "--disallowedTools",
1026
+ ...disallowed,
1027
+ "--",
1028
+ ctx.prompt
1029
+ ];
1030
+ return { args, env: { DISABLE_AUTOUPDATER: "1" } };
1031
+ }
1032
+ case "codex": {
1033
+ const toml = (s) => JSON.stringify(s);
1034
+ const args = [
1035
+ "--no-alt-screen",
1036
+ "-c",
1037
+ `notify=[${[process.execPath, ctx.notify.script, ctx.notify.url].map(toml).join(",")}]`,
1038
+ "-c",
1039
+ `developer_instructions=${toml(ctx.systemAppend)}`,
1040
+ ...ctx.model ? ["-m", ctx.model] : [],
1041
+ ...ctx.effort ? ["-c", `model_reasoning_effort=${toml(EFFORT_CODEX[ctx.effort])}`] : [],
1042
+ ...ctx.browser ? ["-c", `mcp_servers.playwright={command=${toml(ctx.browser.command)},args=[${ctx.browser.args.map(toml).join(",")}]}`] : [],
1043
+ ...CODEX_HOOK_EVENTS.flatMap((e) => ["-c", `hooks.${e}=[{hooks=[{type="command",command=${toml(codexHookCommand(process.execPath, ctx.codexHook))},timeout=10}]}]`]),
1044
+ // Like the manager's own Codex, but it can't stop to ask: no approvals and no sandbox (its sandbox can't reach
1045
+ // the credential store, so git and gh fail in it).
1046
+ "--dangerously-bypass-approvals-and-sandbox"
1047
+ ];
1048
+ return { args: ctx.resumeId ? ["resume", ...args, ctx.resumeId, ctx.prompt] : [...args, "--", ctx.prompt], env: { CUBEFARM_HOOK_URL: ctx.notify.url } };
1049
+ }
1050
+ case "opencode": {
1051
+ const config = {
1052
+ plugin: [ctx.plugin],
1053
+ instructions: [ctx.files.system],
1054
+ autoupdate: false,
1055
+ // several agents starting at once must not each reinstall it
1056
+ permission: { edit: "allow", bash: "allow", webfetch: "allow" },
1057
+ // it can't stop to ask either
1058
+ ...ctx.browser ? { mcp: { playwright: { type: "local", command: [ctx.browser.command, ...ctx.browser.args], enabled: true } } } : {}
1059
+ };
1060
+ const args = [
1061
+ "--auto",
1062
+ ...ctx.model ? ["--model", ctx.model] : [],
1063
+ ...ctx.resumeId ? ["--session", ctx.resumeId] : [],
1064
+ "--prompt",
1065
+ ctx.prompt
1066
+ ];
1067
+ return { args, env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(config), CUBEFARM_NOTIFY_URL: ctx.notify.url } };
1068
+ }
1069
+ }
1070
+ }
1071
+ var TRUST_PROMPT = /Quick safety check|Do you trust the (files|contents) (in|of) this|trust this folder|allow Codex to work in this folder/i;
1072
+ function trustKey(screen) {
1073
+ if (!TRUST_PROMPT.test(screen)) return null;
1074
+ const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
1075
+ if (/^(yes|trust|continue|proceed)\b/i.test(selected.trim())) return "enter";
1076
+ if (/^(no|exit|quit|cancel)\b/i.test(selected.trim())) return "down";
1077
+ return null;
1078
+ }
1079
+ function hookReviewKey(screen) {
1080
+ if (!/Hooks need review/i.test(screen)) return null;
1081
+ const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
1082
+ return /^continue without trusting/i.test(selected.trim()) ? "enter" : "down";
1083
+ }
1084
+ function interruptions(screen) {
1085
+ return screen.split("\n").filter((l) => /\binterrupted\b/i.test(l)).length;
1086
+ }
1087
+ var STATUSLINE_SOURCE = String.raw`// cubefarm: Claude Code's status line. Forwards the session's status to the office and prints the office's line.
1088
+ const url = process.argv[2];
1089
+ let body = '';
1090
+ process.stdin.on('data', (c) => (body += c));
1091
+ process.stdin.on('end', async () => {
1092
+ try {
1093
+ const status = JSON.parse(body);
1094
+ const res = await fetch(url, {
1095
+ method: 'POST',
1096
+ headers: { 'content-type': 'application/json' },
1097
+ body: JSON.stringify({ hook_event_name: 'StatusLine', session_id: status.session_id, cost: status.cost, rate_limits: status.rate_limits }),
1098
+ signal: AbortSignal.timeout(2000),
1099
+ });
1100
+ const out = await res.json();
1101
+ process.stdout.write(String(out.statusLine ?? ''));
1102
+ } catch {
1103
+ process.stdout.write('cubefarm');
1104
+ }
1105
+ });
1106
+ `;
1107
+ var NOTIFY_SOURCE = String.raw`// cubefarm: Codex's notify program. Tells the office a turn is complete.
1108
+ const [url, payload] = process.argv.slice(2);
1109
+ let event = {};
1110
+ try {
1111
+ event = JSON.parse(payload ?? '{}');
1112
+ } catch {}
1113
+ if (event.type === 'agent-turn-complete') {
1114
+ const input = String((event['input-messages'] ?? [])[0] ?? '').slice(0, 300);
1115
+ fetch(url, {
1116
+ method: 'POST',
1117
+ headers: { 'content-type': 'application/json' },
1118
+ body: JSON.stringify({ hook_event_name: 'TurnComplete', session_id: event['thread-id'] ?? null, last_assistant_message: event['last-assistant-message'] ?? '', input }),
1119
+ signal: AbortSignal.timeout(3000),
1120
+ }).catch(() => {});
1121
+ }
1122
+ `;
1123
+ var CODEX_HOOK_SOURCE = String.raw`// cubefarm: Codex's hooks. Tells the office what Codex is doing; never changes what it does.
1124
+ const url = process.env.CUBEFARM_HOOK_URL;
1125
+ const chunks = [];
1126
+ process.stdin.on('data', (c) => chunks.push(c));
1127
+ process.stdin.on('end', async () => {
1128
+ try {
1129
+ if (url) await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: Buffer.concat(chunks), signal: AbortSignal.timeout(5000) });
1130
+ } catch {}
1131
+ process.stdout.write('{}');
1132
+ });
1133
+ `;
1134
+ var OPENCODE_PLUGIN_SOURCE = String.raw`// cubefarm: tells the office when OpenCode's session goes idle.
1135
+ export default async function CubefarmPlugin({ client } = {}) {
1136
+ const url = process.env.CUBEFARM_NOTIFY_URL;
1137
+ if (!url) return {};
1138
+ let root;
1139
+ const post = (body) =>
1140
+ fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(3000) }).catch(() => {});
1141
+ const lastReply = async (id) => {
1142
+ try {
1143
+ const res = await client.session.messages({ path: { id } });
1144
+ const reply = (res?.data ?? []).filter((m) => m?.info?.role === 'assistant').pop();
1145
+ return (reply?.parts ?? []).filter((p) => p?.type === 'text').map((p) => p.text).join('\n').trim();
1146
+ } catch {
1147
+ return '';
1148
+ }
1149
+ };
1150
+ return {
1151
+ event: async ({ event }) => {
1152
+ const props = event?.properties ?? {};
1153
+ if (event?.type === 'session.created' && !props.info?.parentID && !root) root = props.info?.id;
1154
+ const sessionID = props.sessionID ?? props.info?.id;
1155
+ if (event?.type === 'session.idle' && (!root || sessionID === root)) {
1156
+ post({ hook_event_name: 'TurnComplete', session_id: sessionID ?? null, last_assistant_message: sessionID ? await lastReply(sessionID) : '' });
1157
+ }
1158
+ if (event?.type === 'session.error') post({ hook_event_name: 'TurnError', session_id: sessionID ?? null, error: String(props.error?.data?.message ?? props.error?.name ?? 'error') });
1159
+ },
1160
+ };
1161
+ }
1162
+ `;
1163
+
1164
+ // server/ptyClient.ts
1165
+ import crypto2 from "node:crypto";
1166
+ import fs5 from "node:fs";
1167
+ import net from "node:net";
1168
+ import path6 from "node:path";
887
1169
  import { spawn } from "node:child_process";
1170
+
1171
+ // server/ptyProtocol.ts
888
1172
  import crypto from "node:crypto";
889
- import fs3 from "node:fs/promises";
890
- import net from "node:net";
891
- import path4 from "node:path";
1173
+ import path5 from "node:path";
1174
+ var PTY_PROTOCOL = 1;
1175
+ function hostSocket(home, platform = process.platform) {
1176
+ if (platform !== "win32") return path5.join(home, "pty.sock");
1177
+ const tag = crypto.createHash("sha1").update(path5.resolve(home).toLowerCase()).digest("hex").slice(0, 12);
1178
+ return `\\\\.\\pipe\\cubefarm-pty-${tag}`;
1179
+ }
1180
+ var encode = (m) => `${JSON.stringify(m)}
1181
+ `;
1182
+ function readMessages(sock, onMessage2) {
1183
+ let buf = "";
1184
+ sock.setEncoding("utf8");
1185
+ sock.on("data", (chunk) => {
1186
+ buf += chunk;
1187
+ let nl;
1188
+ while ((nl = buf.indexOf("\n")) >= 0) {
1189
+ const line = buf.slice(0, nl);
1190
+ buf = buf.slice(nl + 1);
1191
+ if (!line) continue;
1192
+ let m;
1193
+ try {
1194
+ m = JSON.parse(line);
1195
+ } catch {
1196
+ continue;
1197
+ }
1198
+ onMessage2(m);
1199
+ }
1200
+ });
1201
+ }
1202
+
1203
+ // server/ptyClient.ts
1204
+ var nodePty = await import("@lydell/node-pty").catch((err) => {
1205
+ console.warn(` terminals unavailable: ${err.message}`);
1206
+ return null;
1207
+ });
1208
+ var terminalsAvailable = nodePty !== null;
1209
+ var conn = null;
1210
+ var leaving = false;
1211
+ var hookPort = 0;
1212
+ var hostPid = 0;
1213
+ var found = [];
1214
+ var remote = /* @__PURE__ */ new Map();
1215
+ var hookHandler = () => ({});
1216
+ var send = (m) => {
1217
+ if (conn && !conn.destroyed) conn.write(encode(m));
1218
+ };
1219
+ var RemotePty = class {
1220
+ constructor(id) {
1221
+ this.id = id;
1222
+ remote.set(id, this);
1223
+ }
1224
+ id;
1225
+ pid = 0;
1226
+ dataFns = [];
1227
+ exitFns = [];
1228
+ done = false;
1229
+ write(data) {
1230
+ send({ op: "write", id: this.id, data });
1231
+ }
1232
+ resize(cols, rows) {
1233
+ send({ op: "resize", id: this.id, cols, rows });
1234
+ }
1235
+ kill() {
1236
+ send({ op: "kill", id: this.id });
1237
+ }
1238
+ setMeta(meta) {
1239
+ send({ op: "meta", id: this.id, meta });
1240
+ }
1241
+ onData(cb) {
1242
+ this.dataFns.push(cb);
1243
+ }
1244
+ onExit(cb) {
1245
+ this.exitFns.push(cb);
1246
+ }
1247
+ data(data) {
1248
+ for (const fn of this.dataFns) fn(data);
1249
+ }
1250
+ exit(code) {
1251
+ if (this.done) return;
1252
+ this.done = true;
1253
+ remote.delete(this.id);
1254
+ for (const fn of this.exitFns) fn(code);
1255
+ }
1256
+ };
1257
+ function onMessage(m) {
1258
+ switch (m.op) {
1259
+ case "spawned": {
1260
+ const p = remote.get(m.id);
1261
+ if (p) p.pid = m.pid;
1262
+ return;
1263
+ }
1264
+ case "failed":
1265
+ console.warn(` could not start a terminal: ${m.error}`);
1266
+ return remote.get(m.id)?.exit(-1);
1267
+ case "data":
1268
+ return remote.get(m.id)?.data(m.data);
1269
+ case "exit":
1270
+ return remote.get(m.id)?.exit(m.code);
1271
+ case "hook":
1272
+ void Promise.resolve().then(() => hookHandler(m.token, m.body)).catch(() => ({})).then((answer) => send({ op: "hookReply", rid: m.rid, answer }));
1273
+ return;
1274
+ }
1275
+ }
1276
+ function connect(socket, secret) {
1277
+ return new Promise((resolve) => {
1278
+ const sock = net.connect(socket);
1279
+ let ready = false;
1280
+ const fail = () => {
1281
+ clearTimeout(timer);
1282
+ sock.destroy();
1283
+ resolve(null);
1284
+ };
1285
+ const timer = setTimeout(fail, 2e3);
1286
+ sock.once("error", fail);
1287
+ sock.once("connect", () => sock.write(encode({ op: "hello", secret, version: PTY_PROTOCOL })));
1288
+ readMessages(sock, (m) => {
1289
+ if (ready) return onMessage(m);
1290
+ if (m.op !== "ready") return;
1291
+ ready = true;
1292
+ clearTimeout(timer);
1293
+ sock.removeListener("error", fail);
1294
+ sock.on("error", () => void 0);
1295
+ resolve({ ready: m, sock });
1296
+ });
1297
+ sock.on("close", () => {
1298
+ if (conn !== sock) return;
1299
+ conn = null;
1300
+ if (leaving) return;
1301
+ console.warn(" the terminal keeper stopped; running agents lost their terminals");
1302
+ for (const p of [...remote.values()]) p.exit(-1);
1303
+ });
1304
+ });
1305
+ }
1306
+ var runtimeFlags = () => process.execArgv.filter((a, i, all) => !/^--(inspect|watch|debug)/.test(a) && !/^--(inspect|watch|debug)/.test(all[i - 1] ?? ""));
1307
+ function launch(socket, secretFile) {
1308
+ const entry = path6.join(import.meta.dirname, `ptyHost${path6.extname(import.meta.filename)}`);
1309
+ spawn(process.execPath, [...runtimeFlags(), entry, "launch", socket, secretFile], { detached: true, stdio: "ignore", windowsHide: true }).once("error", () => void 0).unref();
1310
+ }
1311
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1312
+ async function startKeeper(home, onHook) {
1313
+ hookHandler = onHook;
1314
+ if (!nodePty) return [];
1315
+ const socket = hostSocket(home);
1316
+ const secretFile = path6.join(home, "pty.secret");
1317
+ try {
1318
+ if (!fs5.existsSync(secretFile)) {
1319
+ fs5.mkdirSync(home, { recursive: true });
1320
+ fs5.writeFileSync(secretFile, crypto2.randomBytes(24).toString("hex"), { mode: 384 });
1321
+ }
1322
+ } catch (err) {
1323
+ console.warn(` terminal keeper unavailable: ${err.message}`);
1324
+ return [];
1325
+ }
1326
+ const secret = fs5.readFileSync(secretFile, "utf8").trim();
1327
+ let hello = await connect(socket, secret);
1328
+ if (hello && hello.ready.version !== PTY_PROTOCOL) {
1329
+ console.log(` replacing the terminal keeper (v${hello.ready.version}); the ${hello.ready.ptys.length} terminal(s) it held are stopped`);
1330
+ hello.sock.end(encode({ op: "shutdown" }));
1331
+ hello = null;
1332
+ await sleep(3500);
1333
+ }
1334
+ if (!hello) {
1335
+ launch(socket, secretFile);
1336
+ for (let i = 0; i < 30 && !hello; i++) {
1337
+ await sleep(200);
1338
+ hello = await connect(socket, secret);
1339
+ }
1340
+ }
1341
+ if (!hello) {
1342
+ console.warn(" terminal keeper unavailable: agents' terminals run in the office and stop when it restarts");
1343
+ return [];
1344
+ }
1345
+ conn = hello.sock;
1346
+ leaving = false;
1347
+ hookPort = hello.ready.hookPort;
1348
+ hostPid = hello.ready.pid ?? 0;
1349
+ found = hello.ready.ptys;
1350
+ return found;
1351
+ }
1352
+ var keeperPid = () => conn ? hostPid : 0;
1353
+ function keeperHookUrl() {
1354
+ return conn ? `http://127.0.0.1:${hookPort}` : null;
1355
+ }
1356
+ function spawnPty(file, args, o, meta) {
1357
+ if (conn) {
1358
+ const p2 = new RemotePty(crypto2.randomUUID());
1359
+ send({ op: "spawn", id: p2.id, file, args, cwd: o.cwd, env: o.env, cols: o.cols, rows: o.rows, meta });
1360
+ return p2;
1361
+ }
1362
+ if (!nodePty) throw new Error("the terminal module could not be loaded on this machine");
1363
+ const p = nodePty.spawn(file, args, { name: "xterm-256color", cols: o.cols, rows: o.rows, cwd: o.cwd, env: o.env });
1364
+ return {
1365
+ get pid() {
1366
+ return p.pid;
1367
+ },
1368
+ write: (data) => p.write(data),
1369
+ resize: (cols, rows) => p.resize(cols, rows),
1370
+ kill: () => killTree(p),
1371
+ setMeta: () => void 0,
1372
+ onData: (cb) => void p.onData(cb),
1373
+ onExit: (cb) => void p.onExit(({ exitCode }) => cb(exitCode))
1374
+ };
1375
+ }
1376
+ function adoptPty(held) {
1377
+ const p = new RemotePty(held.id);
1378
+ p.pid = held.pid;
1379
+ queueMicrotask(() => send({ op: "attach", id: held.id }));
1380
+ return p;
1381
+ }
1382
+ function discardPty(held) {
1383
+ send({ op: "kill", id: held.id });
1384
+ }
1385
+ function hooksReady() {
1386
+ send({ op: "hooksReady" });
1387
+ }
1388
+ function leaveKeeper() {
1389
+ leaving = true;
1390
+ conn?.end();
1391
+ }
1392
+ function killTree(proc) {
1393
+ if (process.platform === "win32") {
1394
+ const tk = spawn("taskkill", ["/PID", String(proc.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
1395
+ tk.once("error", () => void 0);
1396
+ return;
1397
+ }
1398
+ try {
1399
+ process.kill(-proc.pid, "SIGTERM");
1400
+ } catch {
1401
+ proc.kill();
1402
+ }
1403
+ }
1404
+
1405
+ // server/cliRunner.ts
1406
+ var SESSIONS_DIR = path7.join(HOME_DIR, "sessions");
1407
+ var BIN_DIR = path7.join(HOME_DIR, "bin");
1408
+ var FINISH_GRACE_MS = 3e3;
1409
+ var BOOT_MS = 6e4;
1410
+ var USAGE_WARN_PCT = 90;
1411
+ var INLINE_PROMPT_MAX = 8e3;
1412
+ var officeUrl = "";
1413
+ function setOfficeUrl(url) {
1414
+ officeUrl = url;
1415
+ }
1416
+ var routes = /* @__PURE__ */ new Map();
1417
+ function handleHook(token, body) {
1418
+ const route2 = routes.get(token);
1419
+ if (!route2 || !body || typeof body !== "object" || Array.isArray(body)) return {};
1420
+ try {
1421
+ return route2.hook(body);
1422
+ } catch (err) {
1423
+ console.warn("hook failed", err);
1424
+ return {};
1425
+ }
1426
+ }
1427
+ async function handleMcp(token, req, res) {
1428
+ const office = routes.get(token)?.office;
1429
+ if (!office) {
1430
+ res.status(404).json({ jsonrpc: "2.0", error: { code: -32001, message: "This session has ended." }, id: null });
1431
+ return;
1432
+ }
1433
+ const server2 = office.serve();
1434
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
1435
+ res.on("close", () => {
1436
+ void transport.close();
1437
+ void server2.close();
1438
+ });
1439
+ await server2.connect(transport);
1440
+ await transport.handleRequest(req, res, req.body);
1441
+ }
1442
+ var helpersWritten = false;
1443
+ function writeHelpers() {
1444
+ if (helpersWritten) return;
1445
+ fs6.mkdirSync(BIN_DIR, { recursive: true });
1446
+ fs6.writeFileSync(path7.join(BIN_DIR, "statusline.cjs"), STATUSLINE_SOURCE);
1447
+ fs6.writeFileSync(path7.join(BIN_DIR, "notify.cjs"), NOTIFY_SOURCE);
1448
+ fs6.writeFileSync(path7.join(BIN_DIR, "codex-hook.cjs"), CODEX_HOOK_SOURCE);
1449
+ fs6.writeFileSync(path7.join(BIN_DIR, "opencode-plugin.mjs"), OPENCODE_PLUGIN_SOURCE);
1450
+ helpersWritten = true;
1451
+ }
1452
+ var shellArg = (p) => `"${p.replaceAll("\\", "/")}"`;
1453
+ var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop", "StopFailure", "Notification"];
1454
+ function playwrightServer(outputDir) {
1455
+ const args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--output-dir", outputDir, "--init-script", path7.join(import.meta.dirname, "browser-init.js")];
1456
+ return process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
1457
+ }
1458
+ function toolOutput(response) {
1459
+ const images = [];
1460
+ const blocks = Array.isArray(response) ? response : Array.isArray(response?.content) ? response.content : null;
1461
+ if (blocks) {
1462
+ for (const b of blocks) {
1463
+ const source = b?.source;
1464
+ if (b?.type === "image" && source?.type === "base64" && source.data) images.push({ data: source.data, mime: source.media_type ?? "image/png" });
1465
+ else if (b?.type === "image" && typeof b.data === "string") images.push({ data: b.data, mime: String(b.mimeType ?? "image/png") });
1466
+ }
1467
+ return { text: resultText(blocks), images };
1468
+ }
1469
+ if (typeof response === "string") return { text: response, images };
1470
+ const r = response;
1471
+ if (r && typeof r === "object") {
1472
+ if (typeof r.stdout === "string" || typeof r.stderr === "string") return { text: [r.stdout, r.stderr].filter((s) => typeof s === "string" && s).join("\n"), images };
1473
+ if (typeof r.file?.content === "string") return { text: r.file.content, images };
1474
+ if (Array.isArray(r.filenames)) return { text: r.filenames.join("\n"), images };
1475
+ }
1476
+ return { text: "", images };
1477
+ }
1478
+ function quietly(fn) {
1479
+ try {
1480
+ fn();
1481
+ } catch {
1482
+ }
1483
+ }
1484
+ var lives = /* @__PURE__ */ new Map();
1485
+ var hooksHinted = false;
1486
+ var IDLE_MS = 30 * 6e4;
1487
+ var ALLOW = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", permissionDecisionReason: "Approved by cubefarm" } };
1488
+ var toolInput = (b) => b.tool_input && typeof b.tool_input === "object" ? b.tool_input : {};
1489
+ function endLive(live) {
1490
+ if (lives.get(live.term) !== live) return;
1491
+ lives.delete(live.term);
1492
+ clearTimeout(live.idleTimer);
1493
+ routes.delete(live.token);
1494
+ live.term.releaseIdle = null;
1495
+ live.term.bind(null);
1496
+ live.proc?.kill();
1497
+ live.proc = null;
1498
+ live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
1499
+ fs6.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
1500
+ }
1501
+ var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
1502
+ var remember = (l) => l.proc?.setMeta(metaOf(l));
1503
+ function wire(l, office) {
1504
+ const p = l.proc;
1505
+ lives.set(l.term, l);
1506
+ routes.set(l.token, { hook: (b) => l.session ? l.session.hook(b) : idleHook(l, b), office });
1507
+ l.term.bind({ write: (d) => quietly(() => p.write(d)), resize: (c, r) => quietly(() => p.resize(c, r)) });
1508
+ p.onData((data) => {
1509
+ if (lives.get(l.term) === l) l.term.write(data);
1510
+ });
1511
+ p.onExit((code) => {
1512
+ l.proc = null;
1513
+ if (lives.get(l.term) !== l) return;
1514
+ if (l.session) l.session.exited(code);
1515
+ else endLive(l);
1516
+ });
1517
+ }
1518
+ function waitAtPrompt(l) {
1519
+ clearTimeout(l.idleTimer);
1520
+ l.idleTimer = setTimeout(() => endLive(l), IDLE_MS);
1521
+ l.term.releaseIdle = () => endLive(l);
1522
+ remember(l);
1523
+ }
1524
+ async function reconnectClis(terminalFor) {
1525
+ const held = await startKeeper(HOME_DIR, handleHook);
1526
+ const back = [];
1527
+ for (const h of held) {
1528
+ const meta = h.meta ?? {};
1529
+ const term = meta.agentId ? terminalFor(meta.agentId) : null;
1530
+ if (h.exit !== null || !term || lives.has(term) || !isCli(meta.cli) || !meta.token || !meta.dir || !meta.agentId) {
1531
+ discardPty(h);
1532
+ continue;
1533
+ }
1534
+ const l = { agentId: meta.agentId, cli: meta.cli, term, proc: adoptPty(h), token: meta.token, dir: meta.dir, resumeId: meta.resumeId ?? null, statusLine: "\u{1F3E2} cubefarm", shots: /* @__PURE__ */ new Set(), session: null };
1535
+ wire(l);
1536
+ waitAtPrompt(l);
1537
+ setTimeout(() => quietly(() => (l.proc?.resize(term.cols - 1, term.rows), l.proc?.resize(term.cols, term.rows))), 500);
1538
+ back.push({ agentId: l.agentId, cli: l.cli, resumeId: l.resumeId, busy: !!meta.busy });
1539
+ }
1540
+ return back;
1541
+ }
1542
+ function officeProcesses() {
1543
+ const pids = [keeperPid(), ...[...lives.values()].map((l) => l.proc?.pid ?? 0)].filter((p) => p > 0);
1544
+ return { pids, markers: [SESSIONS_DIR, BIN_DIR] };
1545
+ }
1546
+ function releaseClis(restart) {
1547
+ if (!restart) for (const l of [...lives.values()]) endLive(l);
1548
+ leaveKeeper();
1549
+ }
1550
+ function idleHook(live, b) {
1551
+ switch (String(b.hook_event_name ?? "")) {
1552
+ case "PreToolUse":
1553
+ return ALLOW;
1554
+ case "UserPromptSubmit": {
1555
+ const text = String(b.prompt ?? "").trim();
1556
+ if (!text || live.term.onIdlePrompt?.(text)) return {};
1557
+ return { decision: "block", reason: "The office couldn't take that on as a follow-up right now (see the agent's panel). Nothing was sent." };
1558
+ }
1559
+ case "StatusLine":
1560
+ return { statusLine: live.statusLine };
1561
+ }
1562
+ return {};
1563
+ }
1564
+ function startCliSession(opts, callbacks) {
1565
+ const term = opts.terminal;
1566
+ const prev = lives.get(term);
1567
+ const waiting = !!prev?.proc && !prev.session;
1568
+ const adopt = waiting && (!!opts.reattach || prev.cli === (opts.cli ?? "claude") && !!opts.resumeSessionId && prev.resumeId === opts.resumeSessionId);
1569
+ if (prev && !adopt) endLive(prev);
1570
+ let live = adopt ? prev : null;
1571
+ const cli = live?.cli ?? opts.cli ?? "claude";
1572
+ const label = cliLabel(cli);
1573
+ const token = crypto3.randomUUID();
1574
+ const sessionId = opts.resumeSessionId ?? crypto3.randomUUID();
1575
+ const dir = path7.join(SESSIONS_DIR, token);
1576
+ const started = Date.now();
1577
+ const statusLine = `\u{1F3E2} cubefarm${opts.label ? ` \xB7 ${opts.label}` : ""}`;
1578
+ const toolNames = /* @__PURE__ */ new Map();
1579
+ const officePrompts = [];
1580
+ const launched = [];
1581
+ let mainThread = opts.resumeSessionId ?? null;
1582
+ const usageResets = /* @__PURE__ */ new Map();
1583
+ const warned = /* @__PURE__ */ new Set();
1584
+ let done = false;
1585
+ let costUsd = 0;
1586
+ let turns = 0;
1587
+ let lastText = "";
1588
+ let turnError = null;
1589
+ let begun = false;
1590
+ let finishTimer;
1591
+ let bootTimer;
1592
+ let screenTimer;
1593
+ let shotTimer;
1594
+ const nudged = /* @__PURE__ */ new Set();
1595
+ let trustKeys = 0;
1596
+ let trustedAt = 0;
1597
+ const cb = { ...callbacks };
1598
+ const log = (entries) => entries.length && cb.log(entries);
1599
+ const collectShots = (all = false) => {
1600
+ const l2 = live;
1601
+ if (!l2 || cli === "claude" || !opts.browserTesting) return;
1602
+ for (const shot of newScreenshots(path7.join(l2.dir, "browser"), l2.shots, all ? Infinity : Date.now())) {
1603
+ l2.shots.add(shot.name);
1604
+ try {
1605
+ cb.screenshot(fs6.readFileSync(shot.file), shot.mime);
1606
+ } catch {
1607
+ }
1608
+ }
1609
+ };
1610
+ const finish = (r, keep = false) => {
1611
+ if (done) return;
1612
+ done = true;
1613
+ for (const t of [finishTimer, bootTimer]) clearTimeout(t);
1614
+ clearInterval(screenTimer);
1615
+ clearInterval(shotTimer);
1616
+ collectShots(true);
1617
+ if (term.onInput === watchKeys) term.onInput = null;
1618
+ const l2 = live;
1619
+ if (l2 && lives.get(term) === l2) {
1620
+ l2.session = null;
1621
+ if (keep && opts.keepAlive && l2.proc) waitAtPrompt(l2);
1622
+ else endLive(l2);
1623
+ }
1624
+ cb.tool(null);
1625
+ cb.finished({ ...r, costUsd, turns });
1626
+ };
1627
+ const turnEnded = (text) => {
1628
+ lastText = text;
1629
+ cb.tool(null);
1630
+ clearTimeout(finishTimer);
1631
+ finishTimer = setTimeout(() => finish({ ok: !turnError, text: lastText, errors: turnError ? [turnError] : [] }, true), FINISH_GRACE_MS);
1632
+ };
1633
+ const busy = () => {
1634
+ begun = true;
1635
+ clearTimeout(finishTimer);
1636
+ };
1637
+ const interrupted = () => finish({ ok: false, text: lastText, errors: ["Interrupted in the terminal"], interrupted: true }, true);
1638
+ const watchKeys = (data) => {
1639
+ if (data !== "\x1B" || done || !begun) return;
1640
+ const before = interruptions(term.screen());
1641
+ let looks = 0;
1642
+ const look = () => {
1643
+ if (done) return;
1644
+ if (interruptions(term.screen()) > before) interrupted();
1645
+ else if (++looks < 4) setTimeout(look, 1e3);
1646
+ };
1647
+ setTimeout(look, 800);
1648
+ };
1649
+ term.onInput = watchKeys;
1650
+ const assistantLines = (text) => {
1651
+ const lines = text.split("\n");
1652
+ const out = lines.slice(0, 30).map((l2, i) => ({ kind: "text", text: `${i === 0 ? "\u25CF " : " "}${l2}` }));
1653
+ if (lines.length > 30) out.push({ kind: "text", text: ` \u2026 (${lines.length - 30} more lines)` });
1654
+ return out;
1655
+ };
1656
+ const onStatus = (b) => {
1657
+ const cost = Number(b.cost?.total_cost_usd);
1658
+ if (Number.isFinite(cost)) costUsd = cost;
1659
+ const limits = b.rate_limits ?? {};
1660
+ for (const [type, l2] of Object.entries(limits)) {
1661
+ const used = Number(l2?.used_percentage);
1662
+ if (!Number.isFinite(used)) continue;
1663
+ const resetsAt = Number(l2?.resets_at) > 0 ? Number(l2?.resets_at) * 1e3 : null;
1664
+ usageResets.set(type, used >= 100 ? resetsAt : null);
1665
+ if (used >= USAGE_WARN_PCT && used < 100 && !warned.has(type)) {
1666
+ warned.add(type);
1667
+ cb.usageWarning?.({ resetsAt, rateLimitType: type, utilization: used / 100 });
1668
+ }
1669
+ }
1670
+ return { statusLine };
1671
+ };
1672
+ const hook = (b) => {
1673
+ const event = String(b.hook_event_name ?? "");
1674
+ const sub = typeof b.agent_id === "string" && b.agent_id !== "";
1675
+ switch (event) {
1676
+ case "PreToolUse": {
1677
+ const name = String(b.tool_name ?? "");
1678
+ const input = toolInput(b);
1679
+ if (!sub) {
1680
+ busy();
1681
+ turns += 1;
1682
+ toolNames.set(String(b.tool_use_id ?? ""), name);
1683
+ log([{ kind: "tool", tool: name, text: `\u23FA ${describeTool(opts.cwd, name, input)}` }, ...name === "TodoWrite" ? todoLines(input) : []]);
1684
+ cb.tool(name);
1685
+ if (name === "mcp__playwright__browser_navigate" && typeof input.url === "string") cb.browserUrl(input.url);
1686
+ }
1687
+ return ALLOW;
1688
+ }
1689
+ case "PostToolUse":
1690
+ case "PostToolUseFailure": {
1691
+ if (sub) return {};
1692
+ const name = toolNames.get(String(b.tool_use_id ?? "")) ?? String(b.tool_name ?? "");
1693
+ const out = toolOutput(b.tool_response);
1694
+ if (cli === "claude") for (const img of out.images) cb.screenshot(Buffer.from(img.data, "base64"), img.mime);
1695
+ const saved = name === "mcp__playwright__browser_take_screenshot" && !out.images.length ? screenshotFile(opts.cwd, out.text) : null;
1696
+ if (saved) cb.screenshot(saved.data, saved.mime);
1697
+ if (name.startsWith("mcp__playwright__")) {
1698
+ const url = out.text.match(/Page URL:\s*(\S+)/);
1699
+ if (url) cb.browserUrl(url[1]);
1700
+ }
1701
+ if (event === "PostToolUseFailure") log([{ kind: "error", text: ` \u23BF ${clip(tidyPaths(opts.cwd, String(b.error ?? out.text ?? "Error")).split("\n")[0] || "Error", 200)}` }]);
1702
+ else if (name !== "TodoWrite") log(summariseResult(opts.cwd, name, out.text));
1703
+ cb.tool(null);
1704
+ return {};
1705
+ }
1706
+ case "UserPromptSubmit": {
1707
+ if (sub) return {};
1708
+ busy();
1709
+ const prompt2 = String(b.prompt ?? "").trim();
1710
+ const i = officePrompts.findIndex((p2) => p2.slice(0, 60) === prompt2.slice(0, 60));
1711
+ if (i >= 0) officePrompts.splice(i, 1);
1712
+ else if (prompt2) log([{ kind: "manager", text: `\u25B6 Typed in the terminal: ${clip(prompt2.split("\n")[0], 200)}` }]);
1713
+ return {};
1714
+ }
1715
+ case "Stop": {
1716
+ const text = String(b.last_assistant_message ?? "").trim();
1717
+ turns += 1;
1718
+ if (text) {
1719
+ log(assistantLines(text));
1720
+ cb.turn?.(text);
1721
+ }
1722
+ turnEnded(text);
1723
+ return {};
1724
+ }
1725
+ case "StopFailure": {
1726
+ const error = String(b.error ?? "unknown");
1727
+ const details = String(b.error_details ?? "").trim();
1728
+ if (error === "rate_limit") cb.limited?.([...usageResets.values()].find((t) => t !== null) ?? null);
1729
+ finish({ ok: false, text: lastText, errors: [`${label} stopped: ${error.replace(/_/g, " ")}${details ? ` (${clip(details, 160)})` : ""}`] }, true);
1730
+ return {};
1731
+ }
1732
+ case "Notification": {
1733
+ if (b.notification_type === "permission_prompt") log([{ kind: "error", text: `\u26A0 ${label} is waiting for a permission answer: open the terminal to answer it.` }]);
1734
+ return {};
1735
+ }
1736
+ case "StatusLine":
1737
+ return onStatus(b);
1738
+ case "Interrupt":
1739
+ if (!sub) interrupted();
1740
+ return {};
1741
+ // Codex's notify program and the OpenCode plugin
1742
+ case "TurnComplete": {
1743
+ const thread = typeof b.session_id === "string" ? b.session_id : "";
1744
+ if (cli === "codex" && thread !== mainThread) {
1745
+ const input = String(b.input ?? "").trim();
1746
+ if (mainThread || !launched.some((p2) => input.startsWith(p2.slice(0, 40)))) return {};
1747
+ mainThread = thread;
1748
+ }
1749
+ if (thread) {
1750
+ cb.sessionId(thread);
1751
+ if (live && live.resumeId !== thread) {
1752
+ live.resumeId = thread;
1753
+ remember(live);
1754
+ }
1755
+ }
1756
+ const text = String(b.last_assistant_message ?? "").trim();
1757
+ turns += 1;
1758
+ if (text) {
1759
+ log(assistantLines(text));
1760
+ cb.turn?.(text);
1761
+ }
1762
+ turnEnded(text);
1763
+ return {};
1764
+ }
1765
+ case "TurnError":
1766
+ turnError = `${label}: ${clip(String(b.error ?? "error"), 200)}`;
1767
+ log([{ kind: "error", text: `\u2717 ${turnError}` }]);
1768
+ return {};
1769
+ }
1770
+ return {};
1771
+ };
1772
+ const exited = (exitCode) => {
1773
+ const ok = exitCode === 0 && (lastText !== "" || cli !== "claude" && Date.now() - started > 2e4);
1774
+ finish({ ok, text: lastText, errors: ok ? [] : [`${label} exited${exitCode ? ` with code ${exitCode}` : ""} before finishing.`] });
1775
+ };
1776
+ const handle = {
1777
+ send(text) {
1778
+ const proc = live?.proc;
1779
+ if (done || !proc) return;
1780
+ busy();
1781
+ turnError = null;
1782
+ officePrompts.push(text.trim());
1783
+ const clean = text.replace(/\x1b\[20[01]~/g, "").replace(/\r\n?/g, "\n");
1784
+ launched.push(clean.trim());
1785
+ quietly(() => proc.write(`\x1B[200~${clean}\x1B[201~`));
1786
+ setTimeout(() => quietly(() => live?.proc?.write("\r")), 150);
1787
+ },
1788
+ stop() {
1789
+ finish({ ok: false, text: "", errors: ["Stopped by manager"] });
1790
+ }
1791
+ };
1792
+ if (adopt && live) {
1793
+ clearTimeout(live.idleTimer);
1794
+ term.releaseIdle = null;
1795
+ live.session = { hook, exited };
1796
+ Object.assign(live, { statusLine });
1797
+ remember(live);
1798
+ if (live.resumeId) cb.sessionId(live.resumeId);
1799
+ if (opts.typed || opts.reattach) busy();
1800
+ else handle.send(opts.prompt);
1801
+ shotTimer = setInterval(collectShots, 2e3);
1802
+ return handle;
1803
+ }
1804
+ if (opts.reattach) {
1805
+ setTimeout(() => finish({ ok: false, text: "", errors: [`${label} stopped while the office restarted.`] }), 0);
1806
+ return handle;
1807
+ }
1808
+ const cmd = commandFor(cli);
1809
+ if (!terminalsAvailable || !cmd) {
1810
+ const why = !terminalsAvailable ? "the terminal module could not be loaded on this machine" : `${label} isn't installed (no "${cli}" on PATH)`;
1811
+ setTimeout(() => finish({ ok: false, text: "", errors: [`Could not start ${label}: ${why}.`] }), 0);
1812
+ return handle;
1813
+ }
1814
+ const keeper = keeperHookUrl();
1815
+ const hookUrl = `${keeper ?? officeUrl}/api/hooks/${token}`;
1816
+ const browser = opts.browserTesting ? playwrightServer(path7.join(dir, "browser")) : null;
1817
+ let prompt = opts.prompt;
1818
+ if (opts.outputSchema) {
1819
+ prompt += `
1820
+
1821
+ When you are done, end your final message with your report as one JSON object in a \`\`\`json block, matching this JSON schema:
1822
+ ${JSON.stringify(opts.outputSchema)}`;
1823
+ }
1824
+ let files;
1825
+ try {
1826
+ writeHelpers();
1827
+ fs6.mkdirSync(dir, { recursive: true });
1828
+ const settings = {
1829
+ hooks: Object.fromEntries(HOOK_EVENTS.map((e) => [e, [{ hooks: [{ type: "http", url: hookUrl, timeout: keeper ? 120 : 30 }] }]])),
1830
+ statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(path7.join(BIN_DIR, "statusline.cjs"))} ${shellArg(hookUrl)}`, padding: 0 }
1831
+ };
1832
+ const servers = {};
1833
+ if (browser) servers.playwright = browser;
1834
+ if (opts.office) servers.office = { type: "http", url: `${officeUrl}/api/mcp/${token}` };
1835
+ files = { settings: path7.join(dir, "settings.json"), mcp: Object.keys(servers).length ? path7.join(dir, "mcp.json") : null, system: path7.join(dir, "instructions.md") };
1836
+ fs6.writeFileSync(files.settings, JSON.stringify(settings, null, 2));
1837
+ if (files.mcp) fs6.writeFileSync(files.mcp, JSON.stringify({ mcpServers: servers }, null, 2));
1838
+ fs6.writeFileSync(files.system, opts.systemAppend);
1839
+ if (prompt.length > INLINE_PROMPT_MAX) {
1840
+ const file = path7.join(dir, "task.md");
1841
+ fs6.writeFileSync(file, prompt);
1842
+ prompt = `Your task is in ${file}. Read all of it first, then do what it says.`;
1843
+ }
1844
+ } catch (err) {
1845
+ setTimeout(() => finish({ ok: false, text: "", errors: [`Could not prepare ${label}: ${err.message}`] }), 0);
1846
+ return handle;
1847
+ }
1848
+ officePrompts.push(prompt.trim());
1849
+ launched.push(prompt.trim());
1850
+ const launch2 = launchArgs(cli, {
1851
+ prompt,
1852
+ systemAppend: opts.systemAppend,
1853
+ model: opts.model,
1854
+ effort: opts.effort,
1855
+ resumeId: opts.resumeSessionId,
1856
+ sessionId,
1857
+ name: opts.label ?? "cubefarm",
1858
+ role: opts.role,
1859
+ additionalDirectories: opts.additionalDirectories,
1860
+ files,
1861
+ notify: { script: path7.join(BIN_DIR, "notify.cjs"), url: hookUrl },
1862
+ codexHook: path7.join(BIN_DIR, "codex-hook.cjs"),
1863
+ plugin: pathToFileURL(path7.join(BIN_DIR, "opencode-plugin.mjs")).href,
1864
+ browser
1865
+ });
1866
+ const env = {};
1867
+ for (const [k, v] of Object.entries(process.env)) {
1868
+ if (v === void 0) continue;
1869
+ if (/^(ANTHROPIC_|CLAUDE)/i.test(k) && k !== "CLAUDE_CONFIG_DIR") continue;
1870
+ if (/^(NO_COLOR|FORCE_COLOR|TERM_PROGRAM|TERM_PROGRAM_VERSION|CODEX_THREAD_ID|OPENCODE_CONFIG_CONTENT)$/i.test(k)) continue;
1871
+ env[k] = v;
1872
+ }
1873
+ Object.assign(env, { TERM: "xterm-256color", COLORTERM: "truecolor" }, launch2.env);
1874
+ if (cli === "claude") cb.sessionId(sessionId);
1875
+ term.note(`\u2500\u2500 ${label}${opts.label ? ` \xB7 ${opts.label}` : ""} \u2500\u2500`);
1876
+ const resumeId = cli === "claude" ? sessionId : opts.resumeSessionId ?? null;
1877
+ const l = { agentId: opts.agentId ?? "", cli, term, proc: null, token, dir, resumeId, statusLine, shots: /* @__PURE__ */ new Set(), session: { hook, exited } };
1878
+ let p;
1879
+ try {
1880
+ p = spawnPty(cmd.file, [...cmd.args, ...launch2.args], { cols: term.cols, rows: term.rows, cwd: opts.cwd, env }, metaOf(l));
1881
+ } catch (err) {
1882
+ setTimeout(() => finish({ ok: false, text: "", errors: [`Could not start ${label}: ${err.message}`] }), 0);
1883
+ return handle;
1884
+ }
1885
+ l.proc = p;
1886
+ live = l;
1887
+ wire(l, opts.office);
1888
+ shotTimer = setInterval(collectShots, 2e3);
1889
+ screenTimer = setInterval(() => {
1890
+ if (done || Date.now() - started > BOOT_MS * 2) return clearInterval(screenTimer);
1891
+ const screen = term.screen();
1892
+ const review = hookReviewKey(screen);
1893
+ const key = trustKeys < 12 && Date.now() - trustedAt > 3e3 ? trustKey(screen) ?? review : null;
1894
+ if (key === "down") {
1895
+ trustKeys += 1;
1896
+ quietly(() => p.write(term.appCursor ? "\x1BOB" : "\x1B[B"));
1897
+ } else if (key === "enter") {
1898
+ trustKeys += 1;
1899
+ trustedAt = Date.now();
1900
+ if (!review) log([{ kind: "system", text: `\u2713 Trusted the worktree for ${label}.` }]);
1901
+ else if (!hooksHinted) {
1902
+ hooksHinted = true;
1903
+ log([{ kind: "system", text: `\u2139 To see ${label}'s steps here, trust the office's hooks once: in this terminal, type /hooks and press t. Until then the office shows its task.` }]);
1904
+ }
1905
+ quietly(() => p.write("\r"));
1906
+ }
1907
+ if (!nudged.has("login") && /Not logged in|run \/login|Select login method|Sign in with ChatGPT|Please login|Invalid API key/i.test(screen)) {
1908
+ nudged.add("login");
1909
+ log([{ kind: "error", text: `\u26A0 ${label} needs you to sign in: open the terminal and sign in there, or run "${cli}" in a terminal of your own.` }]);
1910
+ } else if (!nudged.has("setup") && !trustKey(screen) && Date.now() - trustedAt > 5e3 && /Choose the text style|Press Enter to continue|Bypass Permissions mode|Settings Error/i.test(screen)) {
1911
+ nudged.add("setup");
1912
+ log([{ kind: "error", text: `\u26A0 ${label} is waiting on a setup screen: open the terminal to answer it.` }]);
1913
+ }
1914
+ }, 1e3);
1915
+ bootTimer = setTimeout(() => {
1916
+ if (!done && !begun && cli === "claude") log([{ kind: "error", text: `\u26A0 ${label} hasn't started on the task yet. Open the terminal to see what it's waiting for.` }]);
1917
+ }, BOOT_MS);
1918
+ return handle;
1919
+ }
1920
+
1921
+ // server/previewRunner.ts
1922
+ import { spawn as spawn2 } from "node:child_process";
1923
+ import crypto4 from "node:crypto";
1924
+ import fs7 from "node:fs/promises";
1925
+ import net2 from "node:net";
1926
+ import path8 from "node:path";
892
1927
  var PREVIEW_SLUG = "preview";
893
1928
  var PREVIEW_BRANCH = "swarm-preview";
894
1929
  var PREVIEW_TMP = ".preview-tmp";
@@ -909,7 +1944,7 @@ function previewEnv(extra, port, tmp) {
909
1944
  }
910
1945
  async function portOpen(port) {
911
1946
  const probe = (host) => new Promise((resolve) => {
912
- const sock = net.connect({ port, host });
1947
+ const sock = net2.connect({ port, host });
913
1948
  const done = (ok) => {
914
1949
  sock.destroy();
915
1950
  resolve(ok);
@@ -921,10 +1956,10 @@ async function portOpen(port) {
921
1956
  const [v4, v6] = await Promise.all([probe("127.0.0.1"), probe("::1")]);
922
1957
  return v4 || v6;
923
1958
  }
924
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1959
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
925
1960
  async function readJson(file) {
926
1961
  try {
927
- return JSON.parse(await fs3.readFile(file, "utf8"));
1962
+ return JSON.parse(await fs7.readFile(file, "utf8"));
928
1963
  } catch {
929
1964
  return null;
930
1965
  }
@@ -937,11 +1972,11 @@ function defaultCommand(pkg) {
937
1972
  const vite = /^\s*vite(\s|$)/.test(script) && !/--port\b/.test(script);
938
1973
  return `npm run ${name}${vite ? " -- --port {port} --strictPort" : ""}`;
939
1974
  }
940
- function killTree(child) {
1975
+ function killTree2(child) {
941
1976
  if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
942
1977
  if (process.platform === "win32") {
943
1978
  return new Promise((resolve) => {
944
- const tk = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
1979
+ const tk = spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
945
1980
  tk.once("error", () => resolve());
946
1981
  tk.once("close", () => resolve());
947
1982
  });
@@ -962,7 +1997,7 @@ var RealPreview = class {
962
1997
  void this.run().catch((err) => {
963
1998
  if (this.stopped || err instanceof Stopped) return;
964
1999
  this.stopped = true;
965
- if (this.child) void killTree(this.child);
2000
+ if (this.child) void killTree2(this.child);
966
2001
  this.cb.failed(err.message);
967
2002
  });
968
2003
  }
@@ -973,7 +2008,7 @@ var RealPreview = class {
973
2008
  partial = "";
974
2009
  async stop() {
975
2010
  this.stopped = true;
976
- if (this.child) await killTree(this.child);
2011
+ if (this.child) await killTree2(this.child);
977
2012
  }
978
2013
  check() {
979
2014
  if (this.stopped) throw new Stopped();
@@ -985,9 +2020,9 @@ var RealPreview = class {
985
2020
  this.check();
986
2021
  await ensureLocalExcludes(job.fullName).catch(() => void 0);
987
2022
  cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
988
- const tmp = path4.join(wt, PREVIEW_TMP);
989
- await fs3.mkdir(tmp, { recursive: true });
990
- const pkg = await readJson(path4.join(wt, "package.json"));
2023
+ const tmp = path8.join(wt, PREVIEW_TMP);
2024
+ await fs7.mkdir(tmp, { recursive: true });
2025
+ const pkg = await readJson(path8.join(wt, "package.json"));
991
2026
  const command = job.command ?? defaultCommand(pkg);
992
2027
  if (!command) {
993
2028
  this.stopped = true;
@@ -1014,42 +2049,42 @@ var RealPreview = class {
1014
2049
  this.check();
1015
2050
  if (exitCode !== void 0) throw new Error(`The app exited (code ${exitCode}) before it listened on port ${job.port}.`);
1016
2051
  if (Date.now() > deadline) throw new Error(`The app did not listen on port ${job.port} within 3 minutes. Does the command use PORT or {port}?`);
1017
- await sleep(700);
2052
+ await sleep2(700);
1018
2053
  }
1019
2054
  this.check();
1020
2055
  cb.status("running");
1021
2056
  const code = await exited;
1022
- while (!this.stopped && await portOpen(job.port)) await sleep(3e3);
2057
+ while (!this.stopped && await portOpen(job.port)) await sleep2(3e3);
1023
2058
  if (this.stopped) return;
1024
2059
  this.stopped = true;
1025
2060
  cb.failed(`The app stopped (exit code ${code}).`);
1026
2061
  }
1027
2062
  /** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
1028
2063
  async install(wt, env) {
1029
- const lockFile = path4.join(wt, "package-lock.json");
1030
- const lock = await fs3.readFile(lockFile, "utf8").catch(() => null);
1031
- const hash = crypto.createHash("sha256").update(await fs3.readFile(path4.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
1032
- const marker = path4.join(wt, "node_modules", INSTALL_MARKER);
1033
- if (await fs3.readFile(marker, "utf8").catch(() => "") === hash) {
2064
+ const lockFile = path8.join(wt, "package-lock.json");
2065
+ const lock = await fs7.readFile(lockFile, "utf8").catch(() => null);
2066
+ const hash = crypto4.createHash("sha256").update(await fs7.readFile(path8.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
2067
+ const marker = path8.join(wt, "node_modules", INSTALL_MARKER);
2068
+ if (await fs7.readFile(marker, "utf8").catch(() => "") === hash) {
1034
2069
  this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
1035
2070
  return;
1036
2071
  }
1037
2072
  const cmd = lock !== null ? "npm ci" : "npm install";
1038
2073
  this.cb.log([`$ ${cmd}`]);
1039
- const code = await Promise.race([this.spawn(cmd, wt, env), sleep(INSTALL_TIMEOUT_MS).then(() => "timeout")]);
2074
+ const code = await Promise.race([this.spawn(cmd, wt, env), sleep2(INSTALL_TIMEOUT_MS).then(() => "timeout")]);
1040
2075
  this.check();
1041
2076
  if (code === "timeout") {
1042
- if (this.child) await killTree(this.child);
2077
+ if (this.child) await killTree2(this.child);
1043
2078
  throw new Error(`${cmd} took longer than ${INSTALL_TIMEOUT_MS / 6e4} minutes.`);
1044
2079
  }
1045
2080
  if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
1046
2081
  this.child = null;
1047
- await fs3.writeFile(marker, hash).catch(() => void 0);
2082
+ await fs7.writeFile(marker, hash).catch(() => void 0);
1048
2083
  }
1049
2084
  /** Run a command line through the platform shell; resolves with its exit code. */
1050
2085
  spawn(cmd, cwd, env) {
1051
2086
  this.check();
1052
- const child = spawn(cmd, { cwd, env, shell: true, windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
2087
+ const child = spawn2(cmd, { cwd, env, shell: true, windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
1053
2088
  this.child = child;
1054
2089
  const onData = (buf) => {
1055
2090
  const text = this.partial + buf.toString().replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
@@ -1075,12 +2110,12 @@ var RealPreview = class {
1075
2110
  };
1076
2111
  var realPreviews = {
1077
2112
  start: (job, cb) => new RealPreview(job, cb),
1078
- hasDefault: async (fullName) => fs3.access(path4.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
2113
+ hasDefault: async (fullName) => fs7.access(path8.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
1079
2114
  };
1080
2115
 
1081
2116
  // server/officeUpdate.ts
1082
- import fs4 from "node:fs/promises";
1083
- import path5 from "node:path";
2117
+ import fs8 from "node:fs/promises";
2118
+ import path9 from "node:path";
1084
2119
  var DRAIN_TIMEOUT_MS = 20 * 6e4;
1085
2120
  var POSTPONE_MS = 2 * 60 * 6e4;
1086
2121
  function underLauncher(proc = process) {
@@ -1117,10 +2152,10 @@ function parseLastUpdate(raw) {
1117
2152
  }
1118
2153
  }
1119
2154
  async function takeLastUpdate(home) {
1120
- const file = path5.join(home, "last-update.json");
1121
- const raw = await fs4.readFile(file, "utf8").catch(() => null);
2155
+ const file = path9.join(home, "last-update.json");
2156
+ const raw = await fs8.readFile(file, "utf8").catch(() => null);
1122
2157
  if (raw === null) return null;
1123
- await fs4.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
2158
+ await fs8.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
1124
2159
  return parseLastUpdate(raw);
1125
2160
  }
1126
2161
  function lastUpdateMessage(u, commits) {
@@ -1128,8 +2163,8 @@ function lastUpdateMessage(u, commits) {
1128
2163
  const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
1129
2164
  return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
1130
2165
  }
1131
- var OFFICE_DIR = path5.resolve(import.meta.dirname, "..");
1132
- var isSamePath = (a, b) => process.platform === "win32" ? path5.resolve(a).toLowerCase() === path5.resolve(b).toLowerCase() : path5.resolve(a) === path5.resolve(b);
2166
+ var OFFICE_DIR = path9.resolve(import.meta.dirname, "..");
2167
+ var isSamePath = (a, b) => process.platform === "win32" ? path9.resolve(a).toLowerCase() === path9.resolve(b).toLowerCase() : path9.resolve(a) === path9.resolve(b);
1133
2168
  var realOffice = {
1134
2169
  launcher: underLauncher(),
1135
2170
  head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
@@ -1173,16 +2208,21 @@ var realBackend = {
1173
2208
  deskDir,
1174
2209
  prepareDesk,
1175
2210
  removeDesk,
1176
- releaseDesk,
1177
- startSession,
2211
+ releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
2212
+ startSession: (opts, cb) => opts.terminal ? startCliSession(opts, cb) : startSession(opts, cb),
2213
+ terminals: terminalsAvailable,
2214
+ reconnectClis: (terminalFor) => terminalsAvailable ? reconnectClis(terminalFor) : Promise.resolve([]),
2215
+ hooksReady,
2216
+ releaseClis,
2217
+ detectClis,
1178
2218
  previews: realPreviews,
1179
2219
  office: realOffice
1180
2220
  };
1181
2221
 
1182
2222
  // server/demo.ts
1183
- import crypto2 from "node:crypto";
2223
+ import crypto5 from "node:crypto";
1184
2224
  import http from "node:http";
1185
- import path6 from "node:path";
2225
+ import path10 from "node:path";
1186
2226
  var now = () => (/* @__PURE__ */ new Date()).toISOString();
1187
2227
  function issue(n, title, body, fullName, labels = []) {
1188
2228
  return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
@@ -1191,7 +2231,7 @@ var repos = /* @__PURE__ */ new Map();
1191
2231
  var bareRepos = /* @__PURE__ */ new Set();
1192
2232
  var mergedSinceSync = /* @__PURE__ */ new Map();
1193
2233
  var closedIssues = /* @__PURE__ */ new Set();
1194
- var fakeSha = () => crypto2.randomBytes(20).toString("hex");
2234
+ var fakeSha = () => crypto5.randomBytes(20).toString("hex");
1195
2235
  function runChecks(pr, fail = Math.random() < 0.2) {
1196
2236
  Object.assign(pr, { checks: "pending", pendingChecks: ["CI / build", "Vercel"], failedChecks: [] });
1197
2237
  setTimeout(() => {
@@ -1235,7 +2275,7 @@ function screenshotSvg(title, url, hue) {
1235
2275
  </svg>`;
1236
2276
  }
1237
2277
  function devScript(opts, cb, issueNumber, issueTitle) {
1238
- const branch = path6.basename(opts.cwd);
2278
+ const branch = path10.basename(opts.cwd);
1239
2279
  const hue = issueNumber * 67 % 360;
1240
2280
  const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
1241
2281
  const port = 5200 + issueNumber % 50;
@@ -1314,7 +2354,7 @@ function fakeSession(opts, cb, fullName) {
1314
2354
  const title = (kind === "issue" ? issueMatch?.[2] : prMatch?.[2])?.trim() ?? "follow-up";
1315
2355
  const round = Number(opts.prompt.match(/QA round (\d+)/)?.[1] ?? 1);
1316
2356
  const header = [
1317
- { kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort \xB7 ${opts.permissionMode}` },
2357
+ { kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort` },
1318
2358
  { kind: "system", text: ` cwd ${opts.cwd}` }
1319
2359
  ];
1320
2360
  const body = kind === "qa" ? qaScript(cb, number, title, round) : kind === "fix" ? fixScript(number) : devScript(opts, cb, number, title);
@@ -1373,7 +2413,7 @@ function fakeSession(opts, cb, fullName) {
1373
2413
  number: n,
1374
2414
  title,
1375
2415
  url,
1376
- headRefName: `swarm/issue-${number}-${path6.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
2416
+ headRefName: `swarm/issue-${number}-${path10.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
1377
2417
  state: "OPEN",
1378
2418
  isDraft: false,
1379
2419
  mergeable: "MERGEABLE",
@@ -1425,6 +2465,71 @@ function fakeSession(opts, cb, fullName) {
1425
2465
  }
1426
2466
  };
1427
2467
  }
2468
+ var ANSI = {
2469
+ text: (t) => t.replace(/^● /, "\x1B[97m\u25CF\x1B[0m "),
2470
+ tool: (t) => `\x1B[32m\u25CF\x1B[0m \x1B[1m${t.replace(/^⏺ /, "")}\x1B[0m`,
2471
+ result: (t) => `\x1B[2m${t}\x1B[0m`,
2472
+ thinking: (t) => `\x1B[35m${t}\x1B[0m`,
2473
+ error: (t) => `\x1B[31m${t}\x1B[0m`,
2474
+ system: (t) => `\x1B[2m${t}\x1B[0m`,
2475
+ manager: (t) => `\x1B[36m${t.replace(/^▶ /, "\u276F ")}\x1B[0m`,
2476
+ done: (t) => `\x1B[32m${t}\x1B[0m`
2477
+ };
2478
+ function inTerminal(opts, cb, start) {
2479
+ const term = opts.terminal;
2480
+ if (!term) return start(cb);
2481
+ const name = CLIS.find((c) => c.id === (opts.cli ?? "claude"))?.label ?? "Claude Code";
2482
+ term.note(`\u2500\u2500 ${name}${opts.label ? ` \xB7 ${opts.label}` : ""} \u2500\u2500`);
2483
+ term.write(
2484
+ [
2485
+ "",
2486
+ ` \x1B[38;5;209m\u2590\u259B\u2588\u2588\u2588\u259C\u258C\x1B[0m \x1B[1m${name}\x1B[0m (demo)`,
2487
+ `\x1B[38;5;209m\u259D\u259C\u2588\u2588\u2588\u2588\u2588\u259B\u2598\x1B[0m ${opts.model || "default model"} \xB7 ${opts.effort} effort`,
2488
+ `\x1B[38;5;209m \u2598\u2598 \u259D\u259D\x1B[0m \x1B[2m${opts.cwd}\x1B[0m`,
2489
+ "",
2490
+ `\x1B[36m\u276F\x1B[0m ${opts.prompt.split("\n")[0].slice(0, 200)}`,
2491
+ ""
2492
+ ].join("\r\n")
2493
+ );
2494
+ let handle = null;
2495
+ let line = "";
2496
+ term.bind({
2497
+ write: (data) => {
2498
+ for (const ch of data.replace(/\x1b\[[0-9;?]*[A-Za-z~]|\x1b./g, "")) {
2499
+ if (ch === "\r") {
2500
+ const text = line.trim();
2501
+ line = "";
2502
+ term.write("\r\n");
2503
+ if (text && handle) {
2504
+ cb.log([{ kind: "manager", text: `\u25B6 Typed in the terminal: ${text}` }]);
2505
+ handle.send(text);
2506
+ }
2507
+ } else if (ch === "\x7F" || ch === "\b") {
2508
+ if (line) term.write("\b \b");
2509
+ line = line.slice(0, -1);
2510
+ } else if (ch >= " ") {
2511
+ line += ch;
2512
+ term.write(ch);
2513
+ }
2514
+ }
2515
+ },
2516
+ resize: () => void 0
2517
+ });
2518
+ handle = start({
2519
+ ...cb,
2520
+ log: (entries) => {
2521
+ cb.log(entries);
2522
+ for (const e of entries) term.write(`${e.kind === "tool" || e.kind === "text" && e.text.startsWith("\u25CF") ? "\r\n" : ""}${ANSI[e.kind](e.text)}\r
2523
+ `);
2524
+ },
2525
+ finished: (r) => {
2526
+ term.bind(null);
2527
+ term.note(`\u2500\u2500 ${name} session ended \u2500\u2500`);
2528
+ cb.finished(r);
2529
+ }
2530
+ });
2531
+ return handle;
2532
+ }
1428
2533
  var USAGE_WARNING_AT = 4;
1429
2534
  var USAGE_WARNING_MS = 5 * 6e4;
1430
2535
  var sessionsStarted = 0;
@@ -1562,8 +2667,14 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
1562
2667
  releaseDesk: async () => void 0,
1563
2668
  startSession: (opts, cb) => {
1564
2669
  if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
1565
- return opts.role === "ceo" ? ceoSession(opts, cb) : fakeSession(opts, cb, deskRepo.get(opts.cwd) ?? [...repos.keys()][0]);
2670
+ return inTerminal(opts, cb, (c) => opts.role === "ceo" ? ceoSession(opts, c) : fakeSession(opts, c, deskRepo.get(opts.cwd) ?? [...repos.keys()][0]));
1566
2671
  },
2672
+ terminals: true,
2673
+ reconnectClis: async () => [],
2674
+ // fake sessions end with the office
2675
+ hooksReady: () => void 0,
2676
+ releaseClis: () => void 0,
2677
+ detectClis: async () => CLIS.map((c) => ({ id: c.id, label: c.label, installed: true, version: "demo", integrated: c.integrated })),
1567
2678
  previews: demoPreviews,
1568
2679
  office: demoOffice
1569
2680
  };
@@ -1940,9 +3051,9 @@ Acceptance criteria:
1940
3051
  }
1941
3052
 
1942
3053
  // server/swarm.ts
1943
- import fs5 from "node:fs/promises";
1944
- import path7 from "node:path";
1945
- import crypto3 from "node:crypto";
3054
+ import fs10 from "node:fs/promises";
3055
+ import path12 from "node:path";
3056
+ import crypto6 from "node:crypto";
1946
3057
 
1947
3058
  // server/ceo.ts
1948
3059
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
@@ -2109,6 +3220,7 @@ function createOfficeTools(h) {
2109
3220
  const server2 = createSdkMcpServer({ name: "office", version: "1.0.0", tools: defs });
2110
3221
  return {
2111
3222
  server: server2,
3223
+ serve: () => createSdkMcpServer({ name: "office", version: "1.0.0", tools: defs }).instance,
2112
3224
  async call(name, args) {
2113
3225
  const def = defs.find((d) => d.name === name);
2114
3226
  if (!def) throw new Error(`No office tool ${name}`);
@@ -2121,11 +3233,11 @@ function ceoSystemPrompt(o) {
2121
3233
  const manager = o.manager ? `the manager, ${o.manager}` : "the human manager";
2122
3234
  return [
2123
3235
  `You are ${o.name}, the CEO of ${o.company || "an autonomous software company"}, run from an office building called cubefarm. You work from the corner office in the lobby.`,
2124
- `Every floor of the building is one GitHub repository with its own team of Claude Code agents. Developers pick up GitHub issues, each in their own git worktree, and open pull requests. QA testers review and verify every pull request (code review, tests, build, and a real browser via Playwright); when every tester is busy, a free developer who didn't write the PR covers QA. On floors with auto-merge on, the office merges a PR by itself once QA passes and GitHub's checks are green, and sends failing checks or merge conflicts back to a developer; on the others, ${manager} merges. The manager is your board: they approve hires and let-gos.`,
3236
+ `Every floor of the building is one GitHub repository with its own team of AI coding agents. Developers pick up GitHub issues, each in their own git worktree, and open pull requests. QA testers review and verify every pull request (code review, tests, build, and a real browser via Playwright); when every tester is busy, a free developer who didn't write the PR covers QA. On floors with auto-merge on, the office merges a PR by itself once QA passes and GitHub's checks are green, and sends failing checks or merge conflicts back to a developer; on the others, ${manager} merges. The manager is your board: they approve hires and let-gos.`,
2125
3237
  "",
2126
3238
  "Your job is to run the company, not to write code:",
2127
3239
  "- 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.",
2128
- `- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: every agent shares one Claude 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.`,
3240
+ `- 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.`,
2129
3241
  "- 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.",
2130
3242
  "- 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).",
2131
3243
  "",
@@ -2558,6 +3670,197 @@ function pacingMessage(w, until, sessions, now2) {
2558
3670
  return `\u{1F422} Claude's usage is getting high${what ? ` (${what})` : ""}. Until ${clock(until, now2)} the office finishes open work first and starts at most ${sessions} session${sessions === 1 ? "" : "s"} at a time.`;
2559
3671
  }
2560
3672
 
3673
+ // server/terminal.ts
3674
+ import fs9 from "node:fs/promises";
3675
+ import path11 from "node:path";
3676
+ import xtermHeadless from "@xterm/headless";
3677
+ import xtermSerialize from "@xterm/addon-serialize";
3678
+ var { Terminal } = xtermHeadless;
3679
+ var { SerializeAddon } = xtermSerialize;
3680
+ var TERM_COLS = 120;
3681
+ var TERM_ROWS = 32;
3682
+ var SCROLLBACK = 5e3;
3683
+ var STALE_BYTES = 4 * 1024 * 1024;
3684
+ var MOUSE_ENCODINGS = [1006, 1016];
3685
+ var clamp = (n, lo, hi, dflt) => {
3686
+ const v = Math.round(Number(n));
3687
+ return Number.isFinite(v) ? Math.max(lo, Math.min(hi, v)) : dflt;
3688
+ };
3689
+ var AgentTerminal = class {
3690
+ cols = TERM_COLS;
3691
+ rows = TERM_ROWS;
3692
+ /** Output arrived since the last save. */
3693
+ dirty = false;
3694
+ /** Closes a CLI left waiting at its prompt after its task (set by the runtime that left it there). */
3695
+ releaseIdle = null;
3696
+ /** Something the manager typed at that waiting prompt: the office takes it on as a follow-up (true) or refuses. */
3697
+ onIdlePrompt = null;
3698
+ /** Keys a viewer typed, after they went to the CLI (the runtime watches for Esc interrupting a turn). */
3699
+ onInput = null;
3700
+ term = new Terminal({ cols: TERM_COLS, rows: TERM_ROWS, scrollback: SCROLLBACK, allowProposedApi: true, scrollOnEraseInDisplay: true });
3701
+ ser = new SerializeAddon();
3702
+ viewers = /* @__PURE__ */ new Map();
3703
+ sink = null;
3704
+ mouseEncoding = null;
3705
+ resync = null;
3706
+ constructor() {
3707
+ this.term.loadAddon(this.ser);
3708
+ const decset = (on) => (params) => {
3709
+ for (const p of params) if (typeof p === "number" && MOUSE_ENCODINGS.includes(p)) this.mouseEncoding = on ? p : null;
3710
+ return false;
3711
+ };
3712
+ this.term.parser.registerCsiHandler({ prefix: "?", final: "h" }, decset(true));
3713
+ this.term.parser.registerCsiHandler({ prefix: "?", final: "l" }, decset(false));
3714
+ }
3715
+ /** Output from the CLI (or a note from the office): into the mirror and out to everyone watching. */
3716
+ write(data) {
3717
+ if (!data) return;
3718
+ this.term.write(data);
3719
+ this.dirty = true;
3720
+ this.send({ t: "data", data });
3721
+ }
3722
+ /** A dim line from the office itself, e.g. between sessions. */
3723
+ note(text) {
3724
+ this.write(`\r
3725
+ \x1B[2m${text}\x1B[0m\r
3726
+ `);
3727
+ }
3728
+ get live() {
3729
+ return this.sink !== null;
3730
+ }
3731
+ /** The CLI asked for application cursor keys: arrows are sent as ESC O A… rather than ESC [ A… */
3732
+ get appCursor() {
3733
+ return this.term.modes.applicationCursorKeysMode;
3734
+ }
3735
+ /** The running CLI, or null when it exits. */
3736
+ bind(sink) {
3737
+ this.sink = sink;
3738
+ sink?.resize(this.cols, this.rows);
3739
+ this.send({ t: "live", live: sink !== null });
3740
+ }
3741
+ resize(cols, rows) {
3742
+ const c = clamp(cols, 20, 400, this.cols);
3743
+ const r = clamp(rows, 5, 200, this.rows);
3744
+ if (c === this.cols && r === this.rows) return;
3745
+ this.cols = c;
3746
+ this.rows = r;
3747
+ this.term.resize(c, r);
3748
+ this.sink?.resize(c, r);
3749
+ }
3750
+ /** Everything a fresh terminal needs to show the same screen: scrollback, colours, cursor and modes. */
3751
+ snapshot() {
3752
+ return this.ser.serialize({ scrollback: SCROLLBACK }) + (this.mouseEncoding ? `\x1B[?${this.mouseEncoding}h` : "");
3753
+ }
3754
+ /** The visible rows as plain text, for spotting prompts the CLI is waiting on. */
3755
+ screen() {
3756
+ const b = this.term.buffer.active;
3757
+ const rows = [];
3758
+ for (let i = b.viewportY; i < b.viewportY + this.rows; i++) rows.push(b.getLine(i)?.translateToString(true) ?? "");
3759
+ return rows.join("\n");
3760
+ }
3761
+ /** Written to disk so a restarted office still shows what each agent did. */
3762
+ /** Resolves once everything written so far is parsed (xterm parses asynchronously). */
3763
+ flush() {
3764
+ return new Promise((resolve) => this.term.write("", resolve));
3765
+ }
3766
+ async save(file) {
3767
+ this.dirty = false;
3768
+ await this.flush();
3769
+ await fs9.mkdir(path11.dirname(file), { recursive: true });
3770
+ await fs9.writeFile(`${file}.tmp`, this.snapshot());
3771
+ await fs9.rename(`${file}.tmp`, file);
3772
+ }
3773
+ async load(file) {
3774
+ const data = await fs9.readFile(file, "utf8").catch(() => "");
3775
+ if (!data) return;
3776
+ this.term.write(data);
3777
+ this.note("(the office restarted)");
3778
+ this.dirty = false;
3779
+ }
3780
+ // ---------- viewers ----------
3781
+ attach(ws) {
3782
+ const viewer = { stale: false, queued: [] };
3783
+ this.viewers.set(ws, viewer);
3784
+ this.term.write("", () => {
3785
+ const queued = viewer.queued ?? [];
3786
+ viewer.queued = null;
3787
+ this.sendTo(ws, { t: "snapshot", data: this.snapshot(), cols: this.cols, rows: this.rows, live: this.live });
3788
+ for (const json of queued) if (ws.readyState === ws.OPEN) ws.send(json);
3789
+ });
3790
+ ws.on("message", (raw) => {
3791
+ let msg;
3792
+ try {
3793
+ msg = JSON.parse(String(raw));
3794
+ } catch {
3795
+ return;
3796
+ }
3797
+ if (msg.t === "input" && typeof msg.data === "string") {
3798
+ const data = msg.data.slice(0, 64 * 1024);
3799
+ this.sink?.write(data);
3800
+ this.onInput?.(data);
3801
+ } else if (msg.t === "resize") {
3802
+ this.resize(msg.cols, msg.rows);
3803
+ for (const other of this.viewers.keys()) if (other !== ws) this.sendTo(other, { t: "size", cols: this.cols, rows: this.rows });
3804
+ }
3805
+ });
3806
+ ws.on("close", () => this.viewers.delete(ws));
3807
+ }
3808
+ dispose() {
3809
+ for (const ws of this.viewers.keys()) ws.close();
3810
+ this.viewers.clear();
3811
+ if (this.resync) clearTimeout(this.resync);
3812
+ this.sink = null;
3813
+ this.term.dispose();
3814
+ }
3815
+ send(msg) {
3816
+ if (this.viewers.size === 0) return;
3817
+ const json = JSON.stringify(msg);
3818
+ for (const [ws, v] of this.viewers) {
3819
+ if (ws.readyState !== ws.OPEN || v.stale) continue;
3820
+ if (v.queued) {
3821
+ v.queued.push(json);
3822
+ continue;
3823
+ }
3824
+ if (ws.bufferedAmount > STALE_BYTES) {
3825
+ v.stale = true;
3826
+ this.scheduleResync();
3827
+ continue;
3828
+ }
3829
+ ws.send(json);
3830
+ }
3831
+ }
3832
+ sendTo(ws, msg) {
3833
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(msg));
3834
+ }
3835
+ /** Viewers that fell behind get a fresh snapshot once their socket has drained, instead of an ever-growing backlog. */
3836
+ scheduleResync() {
3837
+ if (this.resync) return;
3838
+ this.resync = setTimeout(() => {
3839
+ this.resync = null;
3840
+ let waiting = false;
3841
+ for (const [ws, v] of this.viewers) {
3842
+ if (!v.stale) continue;
3843
+ if (ws.bufferedAmount > STALE_BYTES / 4) {
3844
+ waiting = true;
3845
+ continue;
3846
+ }
3847
+ v.stale = false;
3848
+ this.sendTo(ws, { t: "snapshot", data: this.snapshot(), cols: this.cols, rows: this.rows, live: this.live });
3849
+ }
3850
+ if (waiting) this.scheduleResync();
3851
+ }, 1e3);
3852
+ }
3853
+ };
3854
+
3855
+ // shared/models.ts
3856
+ var isClaudeModel = (model) => /^(claude|opus|sonnet|haiku|fable|default|best|opusplan)\b/i.test(model.trim());
3857
+ function effectiveModel(own, cli, office, claudeDefault) {
3858
+ const suits = (m) => m.trim() !== "" && cli === "claude" === isClaudeModel(m);
3859
+ if (suits(own)) return own.trim();
3860
+ if (cli === office.defaultCli && suits(office.defaultModel)) return office.defaultModel.trim();
3861
+ return cli === "claude" ? claudeDefault : "";
3862
+ }
3863
+
2561
3864
  // shared/types.ts
2562
3865
  var CEO_ID = "ceo";
2563
3866
 
@@ -2605,8 +3908,8 @@ var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
2605
3908
  var CEO_MODEL = "claude-opus-5-5";
2606
3909
  var CEO_EFFORT = "xhigh";
2607
3910
  var CEO_NAME = "Morgan";
2608
- var CEO_DIR = path7.join(HOME_DIR, "ceo");
2609
- var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path7.resolve(import.meta.dirname, ".."));
3911
+ var CEO_DIR = path12.join(HOME_DIR, "ceo");
3912
+ var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path12.resolve(import.meta.dirname, ".."));
2610
3913
  var MAX_PENDING_REQUESTS = 8;
2611
3914
  var MAX_ISSUES_PER_JOB = 12;
2612
3915
  var KEEP_MESSAGES = 200;
@@ -2619,14 +3922,14 @@ var ERROR_COOLDOWN_MS = 2 * 6e4;
2619
3922
  var MAX_ISSUE_FAILURES = 2;
2620
3923
  var LIMIT_PAUSE_MS = 15 * 6e4;
2621
3924
  var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
2622
- var SCREENS_DIR = path7.join(HOME_DIR, "screens");
3925
+ var SCREENS_DIR = path12.join(HOME_DIR, "screens");
2623
3926
  var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
2624
- var screenFile = (agentId, mime) => path7.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
3927
+ var screenFile = (agentId, mime) => path12.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
2625
3928
  async function loadScreen(agentId) {
2626
3929
  for (const [mime, ext] of Object.entries(MIME_EXT)) {
2627
- const file = path7.join(SCREENS_DIR, `${agentId}.${ext}`);
3930
+ const file = path12.join(SCREENS_DIR, `${agentId}.${ext}`);
2628
3931
  try {
2629
- const [data, stat] = await Promise.all([fs5.readFile(file), fs5.stat(file)]);
3932
+ const [data, stat] = await Promise.all([fs10.readFile(file), fs10.stat(file)]);
2630
3933
  return { data, mime, at: stat.mtimeMs };
2631
3934
  } catch {
2632
3935
  }
@@ -2634,8 +3937,11 @@ async function loadScreen(agentId) {
2634
3937
  return null;
2635
3938
  }
2636
3939
  async function removeScreens(agentId) {
2637
- await Promise.all(Object.values(MIME_EXT).map((ext) => fs5.rm(path7.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
3940
+ await Promise.all(Object.values(MIME_EXT).map((ext) => fs10.rm(path12.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
2638
3941
  }
3942
+ var TERMINALS_DIR = path12.join(HOME_DIR, "terminals");
3943
+ var terminalFile = (agentId) => path12.join(TERMINALS_DIR, `${agentId}.ansi`);
3944
+ var TERMINAL_SAVE_MS = 2e4;
2639
3945
  var QA_SCHEMA = {
2640
3946
  type: "object",
2641
3947
  additionalProperties: false,
@@ -2678,10 +3984,11 @@ var QA_SCHEMA = {
2678
3984
  function parseReport(result) {
2679
3985
  let raw = result.structured;
2680
3986
  if (!raw && result.text) {
2681
- const json = result.text.match(/\{[\s\S]*\}/);
2682
- if (json) {
3987
+ const fenced = [...result.text.matchAll(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/g)].map((m) => m[1]).reverse();
3988
+ for (const json of [...fenced, result.text.match(/\{[\s\S]*\}/)?.[0]].filter((j) => !!j)) {
2683
3989
  try {
2684
- raw = JSON.parse(json[0]);
3990
+ raw = JSON.parse(json);
3991
+ break;
2685
3992
  } catch {
2686
3993
  raw = null;
2687
3994
  }
@@ -2702,6 +4009,7 @@ function parseReport(result) {
2702
4009
  var cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
2703
4010
  var ICON = { pass: "\u2705", fail: "\u274C", skip: "\u23ED\uFE0F" };
2704
4011
  var Swarm = class {
4012
+ // coding-agent CLIs found on this machine (detected at startup)
2705
4013
  constructor(backend) {
2706
4014
  this.backend = backend;
2707
4015
  this.previews = new Previews(backend, {
@@ -2718,7 +4026,8 @@ var Swarm = class {
2718
4026
  sessionLimit: 0,
2719
4027
  defaultModel: DEFAULT_MODEL,
2720
4028
  defaultEffort: "medium",
2721
- permissionMode: "guarded",
4029
+ runtime: "terminal",
4030
+ defaultCli: "claude",
2722
4031
  hiring: "approve",
2723
4032
  teamCap: 6,
2724
4033
  ceoHeartbeatMin: 60,
@@ -2781,10 +4090,11 @@ var Swarm = class {
2781
4090
  // sessions cut off by the hand-over are the restarted office's to recover
2782
4091
  };
2783
4092
  lastOfficeView = "";
4093
+ clis = [];
2784
4094
  // ---------- lifecycle ----------
2785
4095
  async init() {
2786
4096
  try {
2787
- const raw = await fs5.readFile(STATE_FILE, "utf8");
4097
+ const raw = await fs10.readFile(STATE_FILE, "utf8");
2788
4098
  const loaded = JSON.parse(raw);
2789
4099
  this.state = {
2790
4100
  settings: { ...this.state.settings, ...loaded.settings },
@@ -2807,7 +4117,9 @@ var Swarm = class {
2807
4117
  brief: a.brief ?? "",
2808
4118
  hiredBy: a.hiredBy ?? "manager",
2809
4119
  look: a.look ?? lookFor(a.name),
2810
- task: a.task ?? (a.issueNumber ? "issue" : null)
4120
+ task: a.task ?? (a.issueNumber ? "issue" : null),
4121
+ cli: isCli(a.cli) ? a.cli : "",
4122
+ sessionCli: a.sessionCli ?? (a.sessionId ? "claude" : null)
2811
4123
  })),
2812
4124
  qa: (loaded.qa ?? []).map((q) => ({
2813
4125
  ...q,
@@ -2827,13 +4139,16 @@ var Swarm = class {
2827
4139
  phoneReadAt: loaded.phoneReadAt ?? 0
2828
4140
  };
2829
4141
  for (const m of this.state.messages) this.messageSeq = Math.max(this.messageSeq, m.id + 1);
2830
- if (!this.state.settings.defaultModel) this.state.settings.defaultModel = DEFAULT_MODEL;
2831
4142
  if (!EFFORTS.includes(this.state.settings.defaultEffort)) this.state.settings.defaultEffort = "medium";
4143
+ if (this.state.settings.runtime !== "sdk") this.state.settings.runtime = "terminal";
4144
+ if (!isCli(this.state.settings.defaultCli)) this.state.settings.defaultCli = "claude";
4145
+ if (!this.state.settings.defaultModel && this.state.settings.defaultCli === "claude") this.state.settings.defaultModel = DEFAULT_MODEL;
2832
4146
  const old = this.state.settings;
2833
4147
  if (old.maxConcurrent !== void 0) {
2834
4148
  if (loaded.settings?.sessionLimit === void 0) old.sessionLimit = old.maxConcurrent === 4 ? 0 : old.maxConcurrent;
2835
4149
  delete old.maxConcurrent;
2836
4150
  }
4151
+ delete old.permissionMode;
2837
4152
  if (loaded.settings && loaded.settings.setupDone === void 0 && this.state.repos.length > 0) {
2838
4153
  Object.assign(this.state.settings, { setupDone: true, tutorialStep: -1 });
2839
4154
  }
@@ -2844,14 +4159,30 @@ var Swarm = class {
2844
4159
  for (const a of this.state.agents) {
2845
4160
  const tail = a.logTail ?? [];
2846
4161
  for (const l of tail) this.logSeq = Math.max(this.logSeq, l.id + 1);
2847
- this.agentRt.set(a.id, { log: tail, pending: [], session: null, currentTool: null, browserUrl: null, screenshot: await loadScreen(a.id), shots: [] });
2848
- if (BUSY.includes(a.status)) {
2849
- a.status = "stopped";
2850
- a.lastError = "The swarm server restarted while this agent was working.";
2851
- interrupted.push(a);
4162
+ this.agentRt.set(a.id, { log: tail, pending: [], session: null, currentTool: null, browserUrl: null, screenshot: await loadScreen(a.id), shots: [], terminal: await this.loadTerminal(a.id) });
4163
+ }
4164
+ const back = await this.backend.reconnectClis((id) => {
4165
+ const a = this.state.agents.find((x) => x.id === id);
4166
+ return a && a.role !== "ceo" ? this.terminalFor(a) : null;
4167
+ }).catch((err) => {
4168
+ console.warn("could not reconnect to the terminal keeper", err);
4169
+ return [];
4170
+ });
4171
+ const carryOn = [];
4172
+ for (const a of this.state.agents) {
4173
+ if (!BUSY.includes(a.status)) continue;
4174
+ if (back.some((c) => c.agentId === a.id && c.busy)) {
4175
+ carryOn.push(a);
4176
+ continue;
2852
4177
  }
4178
+ a.status = "stopped";
4179
+ a.lastError = "The swarm server restarted while this agent was working.";
4180
+ interrupted.push(a);
2853
4181
  }
4182
+ for (const c of back) if (c.busy && !carryOn.some((a) => a.id === c.agentId)) this.agentRt.get(c.agentId)?.terminal?.releaseIdle?.();
2854
4183
  for (const r of this.state.repos) this.repoRt.set(r.id, { issues: [], pulls: [], lastSync: null, syncing: false, cloneStatus: "pending", lastMergedAt: null, folderSync: null, merging: false });
4184
+ for (const a of carryOn) this.reattachSession(a);
4185
+ this.backend.hooksReady();
2855
4186
  try {
2856
4187
  this.user = await this.backend.user();
2857
4188
  } catch (err) {
@@ -2873,7 +4204,8 @@ var Swarm = class {
2873
4204
  await Promise.all([
2874
4205
  ...this.state.agents.map((a) => {
2875
4206
  const repo = this.state.repos.find((r) => r.id === a.repoId);
2876
- return repo ? this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0) : void 0;
4207
+ if (!repo || back.some((c) => c.agentId === a.id)) return void 0;
4208
+ return this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0);
2877
4209
  }),
2878
4210
  this.previews.clearOrphans(this.state.repos)
2879
4211
  ]);
@@ -2884,6 +4216,11 @@ var Swarm = class {
2884
4216
  if (updated) await this.reportUpdate(updated);
2885
4217
  setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
2886
4218
  setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
4219
+ setInterval(() => void this.saveTerminals(), TERMINAL_SAVE_MS);
4220
+ void this.backend.detectClis().then((clis) => {
4221
+ this.clis = clis;
4222
+ this.broadcast({ type: "clis", clis });
4223
+ }).catch((err) => console.warn("could not look for agent CLIs", err));
2887
4224
  this.save();
2888
4225
  setTimeout(() => this.schedule(), 1e3);
2889
4226
  }
@@ -2955,6 +4292,8 @@ var Swarm = class {
2955
4292
  skin: a.skin,
2956
4293
  model: a.model,
2957
4294
  effort: a.effort,
4295
+ cli: a.cli,
4296
+ terminal: !!rt.terminal,
2958
4297
  status: a.status,
2959
4298
  issueNumber: a.issueNumber,
2960
4299
  issueTitle: a.issueTitle,
@@ -3004,6 +4343,7 @@ var Swarm = class {
3004
4343
  messages: this.state.messages.slice(-100),
3005
4344
  phoneReadAt: this.state.phoneReadAt,
3006
4345
  usage: this.usageNow(),
4346
+ clis: this.clis,
3007
4347
  officeCommit: this.officeHead?.slice(0, 7) ?? null,
3008
4348
  officeUpdate: this.officeHead ? this.officeUpdateView() : void 0
3009
4349
  };
@@ -3071,10 +4411,10 @@ var Swarm = class {
3071
4411
  if (this.saveTimer) clearTimeout(this.saveTimer);
3072
4412
  this.saveTimer = null;
3073
4413
  for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
3074
- await fs5.mkdir(path7.dirname(STATE_FILE), { recursive: true });
4414
+ await fs10.mkdir(path12.dirname(STATE_FILE), { recursive: true });
3075
4415
  const tmp = `${STATE_FILE}.tmp`;
3076
- await fs5.writeFile(tmp, JSON.stringify(this.state, null, 2));
3077
- await fs5.rename(tmp, STATE_FILE);
4416
+ await fs10.writeFile(tmp, JSON.stringify(this.state, null, 2));
4417
+ await fs10.rename(tmp, STATE_FILE);
3078
4418
  }
3079
4419
  // ---------- lookups ----------
3080
4420
  repo(id) {
@@ -3156,7 +4496,7 @@ var Swarm = class {
3156
4496
  const name = fullName.split("/")[1];
3157
4497
  const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
3158
4498
  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.`);
3159
- return path7.join(root, name);
4499
+ return path12.join(root, name);
3160
4500
  }
3161
4501
  /** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
3162
4502
  async createRepo(name, opts) {
@@ -3168,11 +4508,11 @@ var Swarm = class {
3168
4508
  }
3169
4509
  /** The folders in your projects folder (or another folder you point at), and which are floors already. */
3170
4510
  async listProjectFolders(dir) {
3171
- const root = dir?.trim() ? path7.resolve(dir.trim()) : this.state.settings.projectsDir;
4511
+ const root = dir?.trim() ? path12.resolve(dir.trim()) : this.state.settings.projectsDir;
3172
4512
  const folders = await this.backend.scanProjects(root).catch((err) => {
3173
4513
  throw new HttpError(400, err.message);
3174
4514
  });
3175
- const same = (a, b) => path7.resolve(a).toLowerCase() === path7.resolve(b).toLowerCase();
4515
+ const same = (a, b) => path12.resolve(a).toLowerCase() === path12.resolve(b).toLowerCase();
3176
4516
  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;
3177
4517
  return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
3178
4518
  }
@@ -3352,7 +4692,7 @@ var Swarm = class {
3352
4692
  rt.folderSync = status;
3353
4693
  this.emitRepo(repo);
3354
4694
  if (status && status !== before) {
3355
- const name = repo.localPath ? path7.basename(repo.localPath) : repo.fullName;
4695
+ const name = repo.localPath ? path12.basename(repo.localPath) : repo.fullName;
3356
4696
  if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
3357
4697
  else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
3358
4698
  }
@@ -3371,9 +4711,14 @@ var Swarm = class {
3371
4711
  stopPreview(id) {
3372
4712
  return this.previews.stop(this.repo(id));
3373
4713
  }
3374
- /** Server shutdown: stop every floor's app so nothing is left holding a preview port. */
3375
- async shutdown() {
4714
+ /**
4715
+ * Server shutdown: stop every floor's app so nothing is left holding a preview port. Agents' CLIs keep working
4716
+ * through a restart (the terminal keeper holds them for the next start) and stop when the office quits.
4717
+ */
4718
+ async shutdown(restart = false) {
3376
4719
  await this.writeState().catch((err) => console.warn("could not save the state", err));
4720
+ this.backend.releaseClis(restart);
4721
+ await this.saveTerminals(true);
3377
4722
  await this.previews.stopAll(this.state.repos);
3378
4723
  }
3379
4724
  async cloneRepo(id) {
@@ -3484,7 +4829,7 @@ var Swarm = class {
3484
4829
  }
3485
4830
  const name = opts.name?.trim() || this.freeName(role);
3486
4831
  const agent = {
3487
- id: crypto3.randomUUID(),
4832
+ id: crypto6.randomUUID(),
3488
4833
  name,
3489
4834
  repoId: repo.id,
3490
4835
  role,
@@ -3500,6 +4845,7 @@ var Swarm = class {
3500
4845
  skin: opts.appearance?.skin ?? pick(SKIN),
3501
4846
  model: opts.model ?? "",
3502
4847
  effort: EFFORTS.includes(opts.effort) ? opts.effort : "",
4848
+ cli: isCli(opts.cli) ? opts.cli : "",
3503
4849
  status: "idle",
3504
4850
  issueNumber: null,
3505
4851
  issueTitle: null,
@@ -3511,11 +4857,12 @@ var Swarm = class {
3511
4857
  costUsd: 0,
3512
4858
  turns: 0,
3513
4859
  sessionId: null,
4860
+ sessionCli: null,
3514
4861
  lastError: null,
3515
4862
  logTail: []
3516
4863
  };
3517
4864
  this.state.agents.push(agent);
3518
- this.agentRt.set(agent.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [] });
4865
+ this.agentRt.set(agent.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [], terminal: null });
3519
4866
  this.appendLog(agent, [
3520
4867
  { kind: "system", text: role === "qa" ? `\u{1F50D} ${name} joined the QA lab on floor ${repo.floor} (${repo.fullName}).` : `\u{1F44B} ${name} joined floor ${repo.floor} (${repo.fullName}).` },
3521
4868
  ...agent.title ? [{ kind: "system", text: `\u{1FAAA} ${agent.title}${agent.specialty ? ` \xB7 takes swarm:${agent.specialty} issues first` : ""}` }] : []
@@ -3538,6 +4885,7 @@ var Swarm = class {
3538
4885
  }
3539
4886
  updateAgent(id, patch) {
3540
4887
  const a = this.agent(id);
4888
+ if (patch.cli !== void 0 && a.role !== "ceo") a.cli = isCli(patch.cli) ? patch.cli : "";
3541
4889
  if (patch.name?.trim() && patch.name.trim() !== a.name) {
3542
4890
  a.name = patch.name.trim().slice(0, 24);
3543
4891
  a.look = lookFor(a.name);
@@ -3565,7 +4913,10 @@ var Swarm = class {
3565
4913
  this.decide(r, { status: "approved", note: "They were let go directly.", decidedBy: "manager" });
3566
4914
  }
3567
4915
  this.agentRt.get(id)?.session?.stop();
4916
+ this.agentRt.get(id)?.terminal?.releaseIdle?.();
4917
+ this.agentRt.get(id)?.terminal?.dispose();
3568
4918
  void removeScreens(id);
4919
+ void fs10.rm(terminalFile(id), { force: true }).catch(() => void 0);
3569
4920
  for (const q of this.state.qa) {
3570
4921
  if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
3571
4922
  if (q.devAgentId === id) q.devAgentId = null;
@@ -3591,6 +4942,20 @@ var Swarm = class {
3591
4942
  this.emitAgent(a);
3592
4943
  this.save();
3593
4944
  }
4945
+ /** An agent whose CLI kept working through the office's restart: the office follows its session again. */
4946
+ reattachSession(a) {
4947
+ const repo = this.state.repos.find((r) => r.id === a.repoId);
4948
+ if (!repo) return;
4949
+ this.appendLog(a, [{ kind: "system", text: "\u21BB The office restarted; their CLI kept working and the office is following it again." }]);
4950
+ this.startAgentSession(a, repo, this.backend.deskDir(repo.fullName, this.agentSlug(a)), "", "", a.sessionId ?? void 0, void 0, "reattach");
4951
+ }
4952
+ /** The manager pressed Esc in the agent's terminal and the CLI stopped its turn: a Stop that leaves the CLI to them. */
4953
+ interrupted(a) {
4954
+ if (!BUSY.includes(a.status)) return;
4955
+ a.status = "stopped";
4956
+ a.lastError = "Interrupted in the terminal";
4957
+ this.appendLog(a, [{ kind: "manager", text: "\u25A0 Interrupted in the terminal. Type there to carry on." }]);
4958
+ }
3594
4959
  resetAgent(id) {
3595
4960
  const a = this.agent(id);
3596
4961
  if (BUSY.includes(a.status)) throw new HttpError(409, `${a.name} is busy; stop them first`);
@@ -3598,6 +4963,7 @@ var Swarm = class {
3598
4963
  if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
3599
4964
  if (q.devAgentId === id && q.status === "fixing") this.setQa(q, { status: "failed" });
3600
4965
  }
4966
+ this.agentRt.get(id)?.terminal?.releaseIdle?.();
3601
4967
  this.clearTask(a);
3602
4968
  this.appendLog(a, [{ kind: "system", text: "\u21BA Cleared desk. Ready for new work." }]);
3603
4969
  this.save();
@@ -3632,6 +4998,57 @@ var Swarm = class {
3632
4998
  linkedRepos(repo) {
3633
4999
  return repo.links.map((id) => this.state.repos.find((r) => r.id === id)).filter((r) => !!r);
3634
5000
  }
5001
+ /**
5002
+ * How an agent's next session runs: the CLI in their terminal (the terminal runtime), or Claude Code through the
5003
+ * SDK. A session can only be resumed by the CLI that made it, so a follow-up stays with that CLI.
5004
+ */
5005
+ sessionRuntime(a, resume) {
5006
+ const inTerminal2 = this.state.settings.runtime === "terminal" && this.backend.terminals;
5007
+ let cli = a.role === "ceo" ? "claude" : a.cli || this.state.settings.defaultCli;
5008
+ if (resume && a.sessionCli && a.sessionCli !== cli) {
5009
+ if (inTerminal2) cli = a.sessionCli;
5010
+ else if (a.sessionCli !== "claude") resume = void 0;
5011
+ }
5012
+ if (!inTerminal2) return { resumeSessionId: resume };
5013
+ const what = a.role === "ceo" ? a.issueTitle : a.task === "qa" ? `QA \xB7 PR #${a.prNumber}` : a.task === "fix" ? `fixing PR #${a.prNumber}` : a.issueNumber ? `#${a.issueNumber} ${a.issueTitle ?? ""}` : null;
5014
+ return { terminal: this.terminalFor(a), cli, label: `${a.name}${what ? ` \xB7 ${what}` : ""}`.slice(0, 80).trim(), resumeSessionId: resume };
5015
+ }
5016
+ /** The model to ask for (the Agent SDK runs Claude Code). '' lets another coding agent use its own default. */
5017
+ modelFor(a, cli) {
5018
+ return effectiveModel(a.model, cli ?? "claude", this.state.settings, DEFAULT_MODEL);
5019
+ }
5020
+ // ---------- terminals ----------
5021
+ async loadTerminal(agentId) {
5022
+ if (!await fs10.stat(terminalFile(agentId)).catch(() => null)) return null;
5023
+ const t = this.newTerminal(agentId);
5024
+ await t.load(terminalFile(agentId));
5025
+ return t;
5026
+ }
5027
+ /** The agent's terminal, made the first time they run in the terminal runtime. */
5028
+ terminalFor(a) {
5029
+ const rt = this.agentRt.get(a.id);
5030
+ rt.terminal ??= this.newTerminal(a.id);
5031
+ return rt.terminal;
5032
+ }
5033
+ newTerminal(agentId) {
5034
+ const t = new AgentTerminal();
5035
+ t.onIdlePrompt = (text) => {
5036
+ void this.message(agentId, text, true).catch(() => void 0);
5037
+ return !!this.agentRt.get(agentId)?.session;
5038
+ };
5039
+ return t;
5040
+ }
5041
+ async saveTerminals(all = false) {
5042
+ for (const [id, rt] of this.agentRt) {
5043
+ if (rt.terminal && (all || rt.terminal.dirty)) await rt.terminal.save(terminalFile(id)).catch((err) => console.warn("could not save a terminal", err));
5044
+ }
5045
+ }
5046
+ /** A browser opened an agent's terminal (/ws/term?agent=<id>). */
5047
+ attachTerminal(agentId, ws) {
5048
+ const t = this.agentRt.get(agentId)?.terminal;
5049
+ if (!t) return ws.close(4404, "That agent has no terminal");
5050
+ t.attach(ws);
5051
+ }
3635
5052
  buildSystemAppend(a, repo, cwd, branch, fixing) {
3636
5053
  const linked = this.linkedRepos(repo).map((r) => `- ${r.fullName}: read-only reference clone at ${this.backend.mainDir(r.fullName)}`);
3637
5054
  const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
@@ -3723,24 +5140,29 @@ Note from the manager: ${note}` : ""
3723
5140
  ].filter(Boolean).join("\n");
3724
5141
  this.startAgentSession(a, repo, cwd, prompt, this.buildSystemAppend(a, repo, cwd, branch));
3725
5142
  }
3726
- startAgentSession(a, repo, cwd, prompt, systemAppend, resumeSessionId, outputSchema) {
5143
+ startAgentSession(a, repo, cwd, prompt, systemAppend, resumeSessionId, outputSchema, mode = null) {
3727
5144
  const rt = this.agentRt.get(a.id);
3728
5145
  a.status = "working";
5146
+ const how = this.sessionRuntime(a, resumeSessionId);
3729
5147
  this.emitAgent(a);
3730
5148
  rt.session = this.backend.startSession(
3731
5149
  {
3732
5150
  cwd,
3733
5151
  prompt,
3734
5152
  systemAppend,
3735
- model: a.model || this.state.settings.defaultModel,
5153
+ model: this.modelFor(a, how.cli),
3736
5154
  effort: a.effort || this.state.settings.defaultEffort,
3737
5155
  browserTesting: repo.browserTesting,
3738
- permissionMode: this.state.settings.permissionMode,
3739
5156
  additionalDirectories: this.linkedRepos(repo).map((r) => this.backend.mainDir(r.fullName)),
3740
5157
  role: a.task === "qa" ? "qa" : a.role,
3741
5158
  // a developer covering QA works under QA's rules
3742
5159
  outputSchema,
3743
- resumeSessionId
5160
+ // A developer's CLI stays at its prompt afterwards, for the manager and for follow-ups; QA's closes.
5161
+ keepAlive: a.task !== "qa",
5162
+ typed: mode === "typed",
5163
+ reattach: mode === "reattach",
5164
+ agentId: a.id,
5165
+ ...how
3744
5166
  },
3745
5167
  {
3746
5168
  log: (entries) => this.appendLog(a, entries),
@@ -3751,6 +5173,7 @@ Note from the manager: ${note}` : ""
3751
5173
  },
3752
5174
  sessionId: (id) => {
3753
5175
  a.sessionId = id;
5176
+ a.sessionCli = how.cli ?? "claude";
3754
5177
  },
3755
5178
  browserUrl: (url) => {
3756
5179
  rt.browserUrl = url;
@@ -3762,13 +5185,12 @@ Note from the manager: ${note}` : ""
3762
5185
  rt.shots.push({ data, mime, url: rt.browserUrl, at });
3763
5186
  if (rt.shots.length > 12) rt.shots.shift();
3764
5187
  this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
3765
- void removeScreens(a.id).then(() => fs5.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs5.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
5188
+ 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));
3766
5189
  },
3767
5190
  limited: (at) => this.pauseForLimit(at),
3768
5191
  usageWarning: (info) => this.paceForWarning(info),
3769
5192
  finished: (result) => void this.onFinished(a, repo, result)
3770
- },
3771
- repo.defaultBranch
5193
+ }
3772
5194
  );
3773
5195
  }
3774
5196
  async onFinished(a, repo, result) {
@@ -3781,6 +5203,7 @@ Note from the manager: ${note}` : ""
3781
5203
  a.costUsd += result.costUsd;
3782
5204
  a.turns += result.turns;
3783
5205
  void this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0);
5206
+ if (result.interrupted) this.interrupted(a);
3784
5207
  if (a.task === "qa") await this.onQaFinished(a, repo, result);
3785
5208
  else if (a.task === "fix") this.onFixFinished(a, repo, result);
3786
5209
  else await this.onIssueFinished(a, repo, result);
@@ -3916,7 +5339,7 @@ ${repo.qaBrief}`] : [],
3916
5339
  "1. Read the PR description and the linked issue, and work out the acceptance criteria.",
3917
5340
  `2. Review the code as a careful reviewer would: git diff origin/${repo.defaultBranch}...HEAD. Look for bugs, unhandled errors and edge cases, security problems, leftover debug code, and new logic without tests.`,
3918
5341
  "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
3919
- repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${this.port(a)} (reserved for you) and exercise the change in a real browser with the Playwright tools: navigate, click, type, resize to a phone size, try edge cases, and check the console for errors. Take a screenshot with browser_take_screenshot of every important state: the screenshots are attached to the PR as evidence. Stop the server afterwards.` : "4. Exercise the changed behaviour directly (run the program, call the API, write a quick script).",
5342
+ repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${this.port(a)} (reserved for you) and exercise the change in a real browser with the Playwright tools: navigate, click, type, resize to a phone size, try edge cases, and check the console for errors. Take a screenshot with browser_take_screenshot (no filename) of every important state: the screenshots are attached to the PR as evidence. Stop the server afterwards.` : "4. Exercise the changed behaviour directly (run the program, call the API, write a quick script).",
3920
5343
  "5. You may write throwaway scripts to probe behaviour, but do not commit them.",
3921
5344
  "",
3922
5345
  "Rules: do not modify the code under test, do not commit, push, comment on, review or merge anything on GitHub. The office posts your report on the pull request. Finish with the structured QA report: verdict, summary, the checks you performed, the commands you ran and one caption per screenshot."
@@ -4153,7 +5576,8 @@ Full report with screenshots: ${rec.commentUrl}` : "",
4153
5576
  this.toast("info", `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`);
4154
5577
  }
4155
5578
  }
4156
- async message(id, text) {
5579
+ /** 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. */
5580
+ async message(id, text, typed = false) {
4157
5581
  const a = this.agent(id);
4158
5582
  if (a.role === "ceo") return this.messageCeo(text);
4159
5583
  const repo = this.repo(a.repoId);
@@ -4161,7 +5585,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
4161
5585
  const rt = this.agentRt.get(id);
4162
5586
  if (rt.session) {
4163
5587
  this.appendLog(a, [{ kind: "manager", text: `\u25B6 Manager: ${text}` }]);
4164
- rt.session.send(text);
5588
+ if (!typed) rt.session.send(text);
4165
5589
  return;
4166
5590
  }
4167
5591
  if (a.role === "qa") throw new HttpError(409, `${a.name} isn't testing anything right now. Send a PR to QA from the Kanban board.`);
@@ -4174,20 +5598,24 @@ Full report with screenshots: ${rec.commentUrl}` : "",
4174
5598
  a.startedAt = Date.now();
4175
5599
  if (a.task === null) a.task = "issue";
4176
5600
  const fixing = a.task === "fix" && a.prNumber ? { pr: a.prNumber, headRef: a.branch } : void 0;
4177
- this.startAgentSession(a, repo, cwd, text, this.buildSystemAppend(a, repo, cwd, a.branch, fixing), a.sessionId);
5601
+ this.startAgentSession(a, repo, cwd, text, this.buildSystemAppend(a, repo, cwd, a.branch, fixing), a.sessionId, void 0, typed ? "typed" : null);
4178
5602
  }
4179
5603
  updateSettings(patch) {
4180
5604
  const s = this.state.settings;
4181
5605
  if (patch.sessionLimit !== void 0) s.sessionLimit = Math.max(0, Math.round(Number(patch.sessionLimit)) || 0);
4182
- if (patch.defaultModel !== void 0) s.defaultModel = String(patch.defaultModel).trim() || DEFAULT_MODEL;
5606
+ if (isCli(patch.defaultCli) && patch.defaultCli !== s.defaultCli) {
5607
+ s.defaultCli = patch.defaultCli;
5608
+ if (patch.defaultModel === void 0) s.defaultModel = s.defaultCli === "claude" ? DEFAULT_MODEL : "";
5609
+ }
5610
+ if (patch.defaultModel !== void 0) s.defaultModel = String(patch.defaultModel).trim() || (s.defaultCli === "claude" ? DEFAULT_MODEL : "");
4183
5611
  if (patch.defaultEffort !== void 0 && EFFORTS.includes(patch.defaultEffort)) s.defaultEffort = patch.defaultEffort;
4184
- if (patch.permissionMode === "guarded" || patch.permissionMode === "bypass") s.permissionMode = patch.permissionMode;
5612
+ if (patch.runtime === "terminal" || patch.runtime === "sdk") s.runtime = patch.runtime;
4185
5613
  if (patch.hiring === "approve" || patch.hiring === "auto") s.hiring = patch.hiring;
4186
5614
  if (patch.teamCap !== void 0) s.teamCap = Math.max(1, Math.min(15, Math.round(Number(patch.teamCap)) || 1));
4187
5615
  if (patch.ceoHeartbeatMin !== void 0) s.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
4188
5616
  if (typeof patch.managerName === "string") s.managerName = patch.managerName.trim().slice(0, 40);
4189
5617
  if (typeof patch.companyName === "string") s.companyName = patch.companyName.trim().slice(0, 60);
4190
- if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir = path7.resolve(patch.projectsDir.trim());
5618
+ if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir = path12.resolve(patch.projectsDir.trim());
4191
5619
  if (typeof patch.setupDone === "boolean") s.setupDone = patch.setupDone;
4192
5620
  if (patch.tutorialStep !== void 0) s.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
4193
5621
  if (typeof patch.autoUpdate === "boolean") s.autoUpdate = patch.autoUpdate;
@@ -4526,6 +5954,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
4526
5954
  skin: pick(SKIN),
4527
5955
  model: CEO_MODEL,
4528
5956
  effort: CEO_EFFORT,
5957
+ cli: "claude",
4529
5958
  status: "idle",
4530
5959
  issueNumber: null,
4531
5960
  issueTitle: null,
@@ -4537,11 +5966,12 @@ Full report with screenshots: ${rec.commentUrl}` : "",
4537
5966
  costUsd: 0,
4538
5967
  turns: 0,
4539
5968
  sessionId: null,
5969
+ sessionCli: null,
4540
5970
  lastError: null,
4541
5971
  logTail: []
4542
5972
  };
4543
5973
  this.state.agents.push(a);
4544
- this.agentRt.set(a.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [] });
5974
+ this.agentRt.set(a.id, { log: [], pending: [], session: null, currentTool: null, browserUrl: null, screenshot: null, shots: [], terminal: null });
4545
5975
  this.appendLog(a, [{ kind: "system", text: `\u{1F3DB}\uFE0F ${a.name} moved into the corner office. The CEO studies every floor, shapes its team and plans its work.` }]);
4546
5976
  }
4547
5977
  const i = interrupted.indexOf(a);
@@ -4631,13 +6061,14 @@ ${job.text}` } : job;
4631
6061
  this.emitAgent(a);
4632
6062
  this.emitCeo();
4633
6063
  this.save();
4634
- await fs5.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
6064
+ await fs10.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
4635
6065
  if (a.status !== "working") {
4636
6066
  this.state.ceo.job = null;
4637
6067
  this.emitCeo();
4638
6068
  return;
4639
6069
  }
4640
6070
  const s = this.state.settings;
6071
+ const how = this.sessionRuntime(a, job.kind === "chat" ? a.sessionId ?? void 0 : void 0);
4641
6072
  rt.session = this.backend.startSession(
4642
6073
  {
4643
6074
  cwd: CEO_DIR,
@@ -4646,7 +6077,7 @@ ${job.text}` } : job;
4646
6077
  name: a.name,
4647
6078
  company: s.companyName,
4648
6079
  manager: s.managerName,
4649
- notesFile: path7.join(CEO_DIR, "NOTES.md"),
6080
+ notesFile: path12.join(CEO_DIR, "NOTES.md"),
4650
6081
  sessionLimit: s.sessionLimit,
4651
6082
  teamCap: s.teamCap,
4652
6083
  hiring: s.hiring
@@ -4654,12 +6085,10 @@ ${job.text}` } : job;
4654
6085
  model: a.model || CEO_MODEL,
4655
6086
  effort: a.effort || CEO_EFFORT,
4656
6087
  browserTesting: false,
4657
- permissionMode: s.permissionMode,
4658
6088
  additionalDirectories: this.state.repos.filter((r) => this.repoRt.get(r.id)?.cloneStatus === "ready").map((r) => this.backend.mainDir(r.fullName)),
4659
6089
  role: "ceo",
4660
6090
  office: this.officeTools(),
4661
- // A chat carries on from the CEO's last session, so "why did you propose that?" has an answer.
4662
- resumeSessionId: job.kind === "chat" ? a.sessionId ?? void 0 : void 0
6091
+ ...how
4663
6092
  },
4664
6093
  {
4665
6094
  log: (entries) => this.appendLog(a, entries),
@@ -4670,6 +6099,7 @@ ${job.text}` } : job;
4670
6099
  },
4671
6100
  sessionId: (id) => {
4672
6101
  a.sessionId = id;
6102
+ a.sessionCli = "claude";
4673
6103
  },
4674
6104
  browserUrl: () => void 0,
4675
6105
  screenshot: () => void 0,
@@ -4677,8 +6107,7 @@ ${job.text}` } : job;
4677
6107
  limited: (at) => this.pauseForLimit(at),
4678
6108
  usageWarning: (info) => this.paceForWarning(info),
4679
6109
  finished: (result) => this.onCeoFinished(a, result)
4680
- },
4681
- ""
6110
+ }
4682
6111
  );
4683
6112
  }
4684
6113
  onCeoFinished(a, result) {
@@ -4691,6 +6120,7 @@ ${job.text}` } : job;
4691
6120
  a.turns += result.turns;
4692
6121
  const job = this.state.ceo.job;
4693
6122
  this.state.ceo.job = null;
6123
+ if (result.interrupted) this.interrupted(a);
4694
6124
  if (a.status === "stopped") {
4695
6125
  } else if (!result.ok) {
4696
6126
  a.status = "error";
@@ -4754,7 +6184,7 @@ ${t}`);
4754
6184
  qa: this.state.qa.map((q) => [q.repoId, q.prNumber, q.status]),
4755
6185
  requests: this.state.requests.filter((r) => r.status === "pending").map((r) => r.id)
4756
6186
  };
4757
- return crypto3.createHash("sha1").update(JSON.stringify(data)).digest("hex");
6187
+ return crypto6.createHash("sha1").update(JSON.stringify(data)).digest("hex");
4758
6188
  }
4759
6189
  maybeHeartbeat() {
4760
6190
  const min = this.state.settings.ceoHeartbeatMin;
@@ -4968,7 +6398,8 @@ ${t}`);
4968
6398
  doing: this.agentDoing(a),
4969
6399
  issue: a.issueNumber ? { number: a.issueNumber, title: a.issueTitle } : null,
4970
6400
  pullRequest: a.prNumber ? { number: a.prNumber, url: a.prUrl } : null,
4971
- model: a.model || (ceo ? CEO_MODEL : this.state.settings.defaultModel),
6401
+ codingAgent: ceo ? "claude" : a.cli || this.state.settings.defaultCli,
6402
+ model: ceo ? a.model || CEO_MODEL : this.modelFor(a, a.cli || this.state.settings.defaultCli) || "the coding agent default",
4972
6403
  effort: a.effort || (ceo ? CEO_EFFORT : this.state.settings.defaultEffort),
4973
6404
  hiredBy: a.hiredBy,
4974
6405
  jobDescription: a.brief || null
@@ -5011,7 +6442,7 @@ ${t}`);
5011
6442
  if (seated + waiting >= MAX_DESKS[role]) throw new Error(role === "qa" ? `The QA lab on floor ${repo.floor} is full.` : `Floor ${repo.floor} has no free desks.`);
5012
6443
  const name = this.freeName(role);
5013
6444
  const req = {
5014
- id: crypto3.randomUUID(),
6445
+ id: crypto6.randomUUID(),
5015
6446
  kind: "hire",
5016
6447
  repoId: repo.id,
5017
6448
  role,
@@ -5053,7 +6484,7 @@ ${t}`);
5053
6484
  if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
5054
6485
  if (pending2.length >= MAX_PENDING_REQUESTS) throw new Error(`${pending2.length} proposals are already waiting for the manager. Wait for their decisions first.`);
5055
6486
  const req = {
5056
- id: crypto3.randomUUID(),
6487
+ id: crypto6.randomUUID(),
5057
6488
  kind: "let-go",
5058
6489
  repoId: repo.id,
5059
6490
  role: a.role,
@@ -5127,8 +6558,12 @@ _Filed by ${this.ceo().name}, the cubefarm CEO._`;
5127
6558
 
5128
6559
  // server/index.ts
5129
6560
  var swarm = new Swarm(DEMO ? createDemoBackend() : realBackend);
6561
+ if (PORT) setOfficeUrl(`http://127.0.0.1:${PORT}`);
5130
6562
  await swarm.init();
5131
6563
  var app = express();
6564
+ app.post("/api/hooks/:token", express.json({ limit: "64mb" }), (req, res) => void res.json(handleHook(String(req.params.token), req.body)));
6565
+ app.post("/api/mcp/:token", express.json({ limit: "4mb" }), (req, res, next) => void handleMcp(String(req.params.token), req, res).catch(next));
6566
+ app.all("/api/mcp/:token", (_req, res) => void res.status(405).set("Allow", "POST").end());
5132
6567
  app.use(express.json({ limit: "1mb" }));
5133
6568
  var route = (fn) => async (req, res, next) => {
5134
6569
  try {
@@ -5248,10 +6683,10 @@ app.post(
5248
6683
  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 }))
5249
6684
  );
5250
6685
  app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
5251
- var dist = path8.resolve(import.meta.dirname, "../dist");
5252
- if (fs6.existsSync(dist)) {
6686
+ var dist = path13.resolve(import.meta.dirname, "../dist");
6687
+ if (fs11.existsSync(dist)) {
5253
6688
  app.use(express.static(dist));
5254
- app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path8.join(dist, "index.html")));
6689
+ app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path13.join(dist, "index.html")));
5255
6690
  }
5256
6691
  app.use((err, _req, res, _next) => {
5257
6692
  const parserStatus = err?.status;
@@ -5261,9 +6696,18 @@ app.use((err, _req, res, _next) => {
5261
6696
  res.status(status).json({ error: message });
5262
6697
  });
5263
6698
  var server = http2.createServer(app);
5264
- var wss = new WebSocketServer({ server, path: "/ws" });
6699
+ var wss = new WebSocketServer({ noServer: true });
6700
+ var terms = new WebSocketServer({ noServer: true });
5265
6701
  wss.on("connection", (ws) => swarm.addClient(ws));
6702
+ terms.on("connection", (ws, req) => swarm.attachTerminal(new URL(req.url ?? "", "http://localhost").searchParams.get("agent") ?? "", ws));
6703
+ server.on("upgrade", (req, socket, head) => {
6704
+ const { pathname } = new URL(req.url ?? "", "http://localhost");
6705
+ const target = pathname === "/ws" ? wss : pathname === "/ws/term" ? terms : null;
6706
+ if (!target) return void socket.destroy();
6707
+ target.handleUpgrade(req, socket, head, (ws) => target.emit("connection", ws, req));
6708
+ });
5266
6709
  server.listen(PORT, "127.0.0.1", () => {
6710
+ setOfficeUrl(`http://127.0.0.1:${server.address().port}`);
5267
6711
  console.log(`
5268
6712
  \u{1F3E2} cubefarm on http://localhost:${PORT}${DEMO ? " (DEMO MODE: fake GitHub + fake agents)" : ""}`);
5269
6713
  console.log(` state: ${STATE_FILE}`);
@@ -5277,16 +6721,18 @@ var shutdown = (signal) => {
5277
6721
  console.log(`
5278
6722
  ${signal}: stopping floor previews\u2026`);
5279
6723
  const force = setTimeout(() => process.exit(0), 15e3);
5280
- void swarm.shutdown().catch((err) => console.error(err)).finally(() => {
6724
+ void swarm.shutdown(signal === "restart").catch((err) => console.error(err)).finally(() => {
5281
6725
  clearTimeout(force);
5282
6726
  process.exit(0);
5283
6727
  });
5284
6728
  };
5285
6729
  process.once("SIGINT", () => shutdown("SIGINT"));
5286
6730
  process.once("SIGTERM", () => shutdown("SIGTERM"));
6731
+ process.once("SIGHUP", () => shutdown("SIGHUP"));
5287
6732
  if (underLauncher()) {
5288
6733
  process.on("message", (msg) => {
5289
- if (msg?.type === "office:shutdown") shutdown("update");
6734
+ const m = msg;
6735
+ if (m?.type === "office:shutdown") shutdown(m.restart === false ? "stop" : "restart");
5290
6736
  });
5291
6737
  }
5292
6738
  //# sourceMappingURL=index.js.map