cubefarm 0.3.12 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/ToyWorld-pH_SiIaW.js +2 -0
- package/dist/assets/index-BjyRl7GR.js +4714 -0
- package/dist/assets/{index-qBB5_EtY.css → index-CqNx66hk.css} +1 -1
- package/dist/assets/rapier-DNlHxnYv.js +1 -0
- package/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/assets/voiceMessages-DuJL7za2.js +1 -0
- package/dist/index.html +3 -2
- package/dist-server/index.js +727 -528
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/assets/ToyWorld-CAwKknk0.js +0 -2
- package/dist/assets/index-BCeYUClc.js +0 -4710
- package/dist/assets/rapier-BRTeZJBn.js +0 -1
package/dist-server/index.js
CHANGED
|
@@ -661,6 +661,80 @@ function sweepDesks(fullName, keep) {
|
|
|
661
661
|
return result;
|
|
662
662
|
});
|
|
663
663
|
}
|
|
664
|
+
var TRIM_DIRS = ["node_modules", "dist", "dist-server", "test-results", "playwright-report", ".swarm-home", ".preview-tmp", ".playwright-mcp"];
|
|
665
|
+
async function dirSize(dir, concurrency = 8) {
|
|
666
|
+
let total = 0;
|
|
667
|
+
const queue = [dir];
|
|
668
|
+
let active = 0;
|
|
669
|
+
const visit = async (d) => {
|
|
670
|
+
const entries = await fs2.readdir(d, { withFileTypes: true }).catch(() => []);
|
|
671
|
+
const sizes = await Promise.all(
|
|
672
|
+
entries.map((e) => {
|
|
673
|
+
const p = path2.join(d, e.name);
|
|
674
|
+
if (e.isDirectory()) queue.push(p);
|
|
675
|
+
return e.isFile() ? fs2.lstat(p).then((s2) => s2.size, () => 0) : 0;
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
for (const s2 of sizes) total += s2;
|
|
679
|
+
};
|
|
680
|
+
await new Promise((resolve) => {
|
|
681
|
+
const next = () => {
|
|
682
|
+
while (active < concurrency && queue.length) {
|
|
683
|
+
active++;
|
|
684
|
+
void visit(queue.pop()).finally(() => {
|
|
685
|
+
active--;
|
|
686
|
+
next();
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (active === 0 && queue.length === 0) resolve();
|
|
690
|
+
};
|
|
691
|
+
next();
|
|
692
|
+
});
|
|
693
|
+
return total;
|
|
694
|
+
}
|
|
695
|
+
async function trimTargets(wt) {
|
|
696
|
+
const ignored = (await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"], { cwd: wt })).split("\0").filter((p) => p.endsWith("/")).map((p) => p.slice(0, -1)).filter((p) => p.includes("/") ? p.endsWith("/node_modules") : TRIM_DIRS.includes(p));
|
|
697
|
+
const present = [];
|
|
698
|
+
for (const name of TRIM_DIRS) if ((await fs2.lstat(path2.join(wt, name)).catch(() => null))?.isDirectory()) present.push(name);
|
|
699
|
+
const candidates = [.../* @__PURE__ */ new Set([...ignored, ...present])];
|
|
700
|
+
if (candidates.length === 0) return [];
|
|
701
|
+
const tracked = (await git(["ls-files", "-z", "--", ...candidates], { cwd: wt })).split("\0").filter(Boolean);
|
|
702
|
+
return candidates.filter((c) => !tracked.some((f) => f.startsWith(`${c}/`)));
|
|
703
|
+
}
|
|
704
|
+
async function trimDesk(fullName, agentSlug, stillIdle = () => true) {
|
|
705
|
+
const wt = deskDir(fullName, agentSlug);
|
|
706
|
+
const trash = path2.join(repoDir(fullName), "trash");
|
|
707
|
+
for (const name of await fs2.readdir(trash).catch(() => [])) {
|
|
708
|
+
if (name.startsWith(`${agentSlug}-`)) await removeDir(path2.join(trash, name)).catch(() => void 0);
|
|
709
|
+
}
|
|
710
|
+
const bin = path2.join(trash, `${agentSlug}-${Date.now()}`);
|
|
711
|
+
const moved = await withRepoLock(fullName, async () => {
|
|
712
|
+
if (!stillIdle() || !await exists(path2.join(wt, ".git"))) return null;
|
|
713
|
+
const out = { removed: [], skipped: [] };
|
|
714
|
+
const targets = await trimTargets(wt);
|
|
715
|
+
for (const [i, rel2] of targets.entries()) {
|
|
716
|
+
if (!stillIdle()) break;
|
|
717
|
+
try {
|
|
718
|
+
await fs2.mkdir(bin, { recursive: true });
|
|
719
|
+
await fs2.rename(path2.join(wt, rel2), path2.join(bin, `${i}-${path2.basename(rel2)}`));
|
|
720
|
+
out.removed.push(rel2);
|
|
721
|
+
} catch {
|
|
722
|
+
out.skipped.push(rel2);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
});
|
|
727
|
+
if (!moved) return null;
|
|
728
|
+
if (moved.removed.length === 0) return { freed: 0, ...moved };
|
|
729
|
+
const size = await dirSize(bin);
|
|
730
|
+
let left = 0;
|
|
731
|
+
try {
|
|
732
|
+
await removeDir(bin);
|
|
733
|
+
} catch {
|
|
734
|
+
left = await dirSize(bin);
|
|
735
|
+
}
|
|
736
|
+
return { freed: size - left, ...moved };
|
|
737
|
+
}
|
|
664
738
|
var LAUNCHERS = ["node.exe", "cmd.exe", "bash.exe", "sh.exe", "conhost.exe", "python.exe", "npm.exe", "npx.exe", "bun.exe", "deno.exe"];
|
|
665
739
|
var pathForms = (p) => [.../* @__PURE__ */ new Set([p, p.replaceAll("\\", "/"), p.replaceAll("\\", "\\\\")])];
|
|
666
740
|
function leftoversInDesk(listing, desk, keep) {
|
|
@@ -725,8 +799,8 @@ Write-Output $kill.Count`;
|
|
|
725
799
|
}
|
|
726
800
|
|
|
727
801
|
// server/agentRunner.ts
|
|
728
|
-
import
|
|
729
|
-
import
|
|
802
|
+
import fs5 from "node:fs";
|
|
803
|
+
import path5 from "node:path";
|
|
730
804
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
731
805
|
|
|
732
806
|
// server/browser.ts
|
|
@@ -764,129 +838,403 @@ function browserProblem(mcp, chrome = hasChrome()) {
|
|
|
764
838
|
return null;
|
|
765
839
|
}
|
|
766
840
|
|
|
767
|
-
// server/
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (
|
|
789
|
-
|
|
841
|
+
// server/clis.ts
|
|
842
|
+
import fs4 from "node:fs";
|
|
843
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
844
|
+
import path4 from "node:path";
|
|
845
|
+
var CLIS = [
|
|
846
|
+
{ id: "claude", label: "Claude Code", command: "claude", integrated: true },
|
|
847
|
+
{ id: "codex", label: "Codex", command: "codex", integrated: false },
|
|
848
|
+
{ id: "opencode", label: "OpenCode", command: "opencode", integrated: false }
|
|
849
|
+
];
|
|
850
|
+
var isCli = (v) => CLIS.some((c) => c.id === v);
|
|
851
|
+
var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
|
|
852
|
+
var WIN = process.platform === "win32";
|
|
853
|
+
function resolveCommand(name, env = process.env) {
|
|
854
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
855
|
+
const dirs = (env[key] ?? "").split(path4.delimiter).filter(Boolean);
|
|
856
|
+
const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
|
|
857
|
+
for (const dir of dirs) {
|
|
858
|
+
for (const ext of exts) {
|
|
859
|
+
const file = path4.join(dir, name + ext.toLowerCase());
|
|
860
|
+
try {
|
|
861
|
+
const st = fs4.statSync(file);
|
|
862
|
+
if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
|
|
863
|
+
} catch {
|
|
790
864
|
}
|
|
791
|
-
}
|
|
865
|
+
}
|
|
792
866
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
function
|
|
796
|
-
const
|
|
797
|
-
if (!
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
function unwrapCmdShim(shimPath, text) {
|
|
870
|
+
const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
|
|
871
|
+
if (!line) return null;
|
|
872
|
+
const dir = path4.dirname(shimPath);
|
|
873
|
+
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path4.join(dir, m[1]));
|
|
874
|
+
const target = targets[targets.length - 1];
|
|
875
|
+
if (!target) return null;
|
|
876
|
+
if (/\.exe$/i.test(target)) return { file: target, args: [] };
|
|
877
|
+
if (!/\.[cm]?js$/i.test(target)) return null;
|
|
878
|
+
const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
|
|
879
|
+
const nodeFlags = line.includes('"%_prog%"') ? between.trim().split(/\s+/).filter((f) => f.startsWith("--")) : [];
|
|
880
|
+
const localNode = path4.join(dir, "node.exe");
|
|
881
|
+
return { file: fs4.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
|
|
882
|
+
}
|
|
883
|
+
var bundled;
|
|
884
|
+
function bundledClaude() {
|
|
885
|
+
if (bundled !== void 0) return bundled;
|
|
886
|
+
bundled = null;
|
|
887
|
+
let sdk;
|
|
798
888
|
try {
|
|
799
|
-
|
|
889
|
+
sdk = createRequire2(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
800
890
|
} catch {
|
|
801
891
|
return null;
|
|
802
892
|
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
893
|
+
const { platform, arch } = process;
|
|
894
|
+
const report = platform === "linux" ? process.report?.getReport() : void 0;
|
|
895
|
+
const musl = platform === "linux" && !report?.header?.glibcVersionRuntime;
|
|
896
|
+
const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
897
|
+
for (const target of targets) {
|
|
898
|
+
try {
|
|
899
|
+
bundled = createRequire2(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
|
|
900
|
+
return bundled;
|
|
901
|
+
} catch {
|
|
902
|
+
}
|
|
811
903
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
function commandFor(id) {
|
|
907
|
+
const bundled2 = id === "claude" ? bundledClaude() : null;
|
|
908
|
+
if (bundled2) return { file: bundled2, args: [] };
|
|
909
|
+
const def = CLIS.find((c) => c.id === id);
|
|
910
|
+
const found2 = def && resolveCommand(def.command);
|
|
911
|
+
if (!found2) return null;
|
|
912
|
+
if (WIN && /\.(cmd|bat)$/i.test(found2)) {
|
|
817
913
|
try {
|
|
818
|
-
|
|
914
|
+
return unwrapCmdShim(found2, fs4.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
|
|
819
915
|
} catch {
|
|
820
|
-
|
|
916
|
+
return { file: found2, args: [] };
|
|
821
917
|
}
|
|
822
|
-
found2.push({ name, file, mime });
|
|
823
918
|
}
|
|
824
|
-
return found2;
|
|
919
|
+
return { file: found2, args: [] };
|
|
825
920
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
921
|
+
async function detectClis() {
|
|
922
|
+
return Promise.all(
|
|
923
|
+
CLIS.map(async (c) => {
|
|
924
|
+
const cmd = commandFor(c.id);
|
|
925
|
+
const version = cmd ? await run(cmd.file, [...cmd.args, "--version"], { timeoutMs: 2e4 }).catch(() => null) : null;
|
|
926
|
+
return { id: c.id, label: c.label, installed: !!cmd, version: version?.split(/\r?\n/)[0].trim().slice(0, 60) || null, integrated: c.integrated };
|
|
927
|
+
})
|
|
928
|
+
);
|
|
832
929
|
}
|
|
833
|
-
var
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
out = out.replace(new RegExp(`^cd\\s+"?${escapeRe(v)}"?\\s*(&&|;)\\s*`, "i"), "");
|
|
838
|
-
out = out.replace(new RegExp(`${escapeRe(v)}[\\\\/]?`, "gi"), "./");
|
|
839
|
-
}
|
|
840
|
-
return out;
|
|
930
|
+
var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
|
|
931
|
+
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
|
|
932
|
+
function codexHookCommand(node, script, win = WIN) {
|
|
933
|
+
return win ? `node "${script}"` : [node, script].map((s2) => `'${s2.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
841
934
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
case "
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
return
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
935
|
+
var CROSS_TURN_TOOLS = ["Monitor", "ScheduleWakeup", "CronCreate", "CronDelete", "CronList", "RemoteTrigger"];
|
|
936
|
+
function launchArgs(id, ctx) {
|
|
937
|
+
switch (id) {
|
|
938
|
+
case "claude": {
|
|
939
|
+
const disallowed = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", ...CROSS_TURN_TOOLS, ...ctx.role === "ceo" ? ["Bash", "PowerShell", "NotebookEdit"] : []];
|
|
940
|
+
const args = [
|
|
941
|
+
...ctx.resumeId ? ["--resume", ctx.resumeId] : ["--session-id", ctx.sessionId],
|
|
942
|
+
...ctx.model ? ["--model", ctx.model] : [],
|
|
943
|
+
...ctx.effort ? ["--effort", ctx.effort] : [],
|
|
944
|
+
// The office's PreToolUse hook answers every permission question (allow, or deny with an office rule).
|
|
945
|
+
"--permission-mode",
|
|
946
|
+
"acceptEdits",
|
|
947
|
+
// On top of the manager's own setup (settings, skills, plugins, MCP servers): the office's hooks and servers.
|
|
948
|
+
"--settings",
|
|
949
|
+
ctx.files.settings,
|
|
950
|
+
...ctx.files.mcp ? ["--mcp-config", ctx.files.mcp] : [],
|
|
951
|
+
"--append-system-prompt-file",
|
|
952
|
+
ctx.files.system,
|
|
953
|
+
"--name",
|
|
954
|
+
ctx.name,
|
|
955
|
+
...ctx.additionalDirectories.flatMap((d) => ["--add-dir", d]),
|
|
956
|
+
"--disallowedTools",
|
|
957
|
+
...disallowed,
|
|
958
|
+
"--",
|
|
959
|
+
ctx.prompt
|
|
960
|
+
];
|
|
961
|
+
return { args, env: { DISABLE_AUTOUPDATER: "1" } };
|
|
962
|
+
}
|
|
963
|
+
case "codex": {
|
|
964
|
+
const toml = (s2) => JSON.stringify(s2);
|
|
965
|
+
const args = [
|
|
966
|
+
"--no-alt-screen",
|
|
967
|
+
"-c",
|
|
968
|
+
`notify=[${[process.execPath, ctx.notify.script, ctx.notify.url].map(toml).join(",")}]`,
|
|
969
|
+
"-c",
|
|
970
|
+
`developer_instructions=${toml(ctx.systemAppend)}`,
|
|
971
|
+
...ctx.model ? ["-m", ctx.model] : [],
|
|
972
|
+
...ctx.effort ? ["-c", `model_reasoning_effort=${toml(EFFORT_CODEX[ctx.effort])}`] : [],
|
|
973
|
+
...ctx.browser ? ["-c", `mcp_servers.playwright={command=${toml(ctx.browser.command)},args=[${ctx.browser.args.map(toml).join(",")}]}`] : [],
|
|
974
|
+
...CODEX_HOOK_EVENTS.flatMap((e) => ["-c", `hooks.${e}=[{hooks=[{type="command",command=${toml(codexHookCommand(process.execPath, ctx.codexHook))},timeout=10}]}]`]),
|
|
975
|
+
// Like the manager's own Codex, but it can't stop to ask: no approvals and no sandbox (its sandbox can't reach
|
|
976
|
+
// the credential store, so git and gh fail in it).
|
|
977
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
978
|
+
];
|
|
979
|
+
return { args: ctx.resumeId ? ["resume", ...args, ctx.resumeId, ctx.prompt] : [...args, "--", ctx.prompt], env: { CUBEFARM_HOOK_URL: ctx.notify.url } };
|
|
980
|
+
}
|
|
981
|
+
case "opencode": {
|
|
982
|
+
const config = {
|
|
983
|
+
plugin: [ctx.plugin],
|
|
984
|
+
instructions: [ctx.files.system],
|
|
985
|
+
autoupdate: false,
|
|
986
|
+
// several agents starting at once must not each reinstall it
|
|
987
|
+
permission: { edit: "allow", bash: "allow", webfetch: "allow" },
|
|
988
|
+
// it can't stop to ask either
|
|
989
|
+
...ctx.browser ? { mcp: { playwright: { type: "local", command: [ctx.browser.command, ...ctx.browser.args], enabled: true } } } : {}
|
|
990
|
+
};
|
|
991
|
+
const args = [
|
|
992
|
+
"--auto",
|
|
993
|
+
...ctx.model ? ["--model", ctx.model] : [],
|
|
994
|
+
...ctx.resumeId ? ["--session", ctx.resumeId] : [],
|
|
995
|
+
"--prompt",
|
|
996
|
+
ctx.prompt
|
|
997
|
+
];
|
|
998
|
+
return { args, env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(config), CUBEFARM_NOTIFY_URL: ctx.notify.url } };
|
|
872
999
|
}
|
|
873
1000
|
}
|
|
874
|
-
if (name.startsWith("mcp__playwright__")) {
|
|
875
|
-
const action = name.replace("mcp__playwright__browser_", "").replace("mcp__playwright__", "");
|
|
876
|
-
const detail = input.url ?? input.element ?? input.text ?? "";
|
|
877
|
-
return `\u{1F310} ${action}${detail ? ` ${clip(String(detail), 120)}` : ""}`;
|
|
878
|
-
}
|
|
879
|
-
if (name.startsWith("mcp__office__")) return describeOfficeTool(name.slice("mcp__office__".length), input);
|
|
880
|
-
return `${name}(${clip(JSON.stringify(input), 140)})`;
|
|
881
1001
|
}
|
|
882
|
-
function
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1002
|
+
function oneAtATime() {
|
|
1003
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1004
|
+
return (key, step) => {
|
|
1005
|
+
const next = (tails.get(key) ?? Promise.resolve()).then(step).then(
|
|
1006
|
+
() => void 0,
|
|
1007
|
+
() => void 0
|
|
1008
|
+
);
|
|
1009
|
+
tails.set(key, next);
|
|
1010
|
+
void next.then(() => tails.get(key) === next && tails.delete(key));
|
|
1011
|
+
return next;
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
var threadSteps = oneAtATime();
|
|
1015
|
+
function codexThread(action, id, after) {
|
|
1016
|
+
return threadSteps(id, async () => {
|
|
1017
|
+
await after;
|
|
1018
|
+
const cmd = commandFor("codex");
|
|
1019
|
+
if (cmd) await run(cmd.file, [...cmd.args, action, id], { timeoutMs: 3e4 });
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
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;
|
|
1023
|
+
function trustKey(screen) {
|
|
1024
|
+
if (!TRUST_PROMPT.test(screen)) return null;
|
|
1025
|
+
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
1026
|
+
if (/^(yes|trust|continue|proceed)\b/i.test(selected.trim())) return "enter";
|
|
1027
|
+
if (/^(no|exit|quit|cancel)\b/i.test(selected.trim())) return "down";
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
function hookReviewKey(screen) {
|
|
1031
|
+
if (!/Hooks need review/i.test(screen)) return null;
|
|
1032
|
+
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
1033
|
+
return /^continue without trusting/i.test(selected.trim()) ? "enter" : "down";
|
|
1034
|
+
}
|
|
1035
|
+
function interruptions(screen) {
|
|
1036
|
+
return screen.split("\n").filter((l) => /\binterrupted\b/i.test(l)).length;
|
|
1037
|
+
}
|
|
1038
|
+
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.
|
|
1039
|
+
const url = process.argv[2];
|
|
1040
|
+
let body = '';
|
|
1041
|
+
process.stdin.on('data', (c) => (body += c));
|
|
1042
|
+
process.stdin.on('end', async () => {
|
|
1043
|
+
try {
|
|
1044
|
+
const status = JSON.parse(body);
|
|
1045
|
+
const res = await fetch(url, {
|
|
1046
|
+
method: 'POST',
|
|
1047
|
+
headers: { 'content-type': 'application/json' },
|
|
1048
|
+
body: JSON.stringify({ hook_event_name: 'StatusLine', session_id: status.session_id, cost: status.cost, rate_limits: status.rate_limits }),
|
|
1049
|
+
signal: AbortSignal.timeout(2000),
|
|
1050
|
+
});
|
|
1051
|
+
const out = await res.json();
|
|
1052
|
+
process.stdout.write(String(out.statusLine ?? ''));
|
|
1053
|
+
} catch {
|
|
1054
|
+
process.stdout.write('cubefarm');
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
`;
|
|
1058
|
+
var NOTIFY_SOURCE = String.raw`// cubefarm: Codex's notify program. Tells the office a turn is complete.
|
|
1059
|
+
const [url, payload] = process.argv.slice(2);
|
|
1060
|
+
let event = {};
|
|
1061
|
+
try {
|
|
1062
|
+
event = JSON.parse(payload ?? '{}');
|
|
1063
|
+
} catch {}
|
|
1064
|
+
if (event.type === 'agent-turn-complete') {
|
|
1065
|
+
const input = String((event['input-messages'] ?? [])[0] ?? '').slice(0, 300);
|
|
1066
|
+
fetch(url, {
|
|
1067
|
+
method: 'POST',
|
|
1068
|
+
headers: { 'content-type': 'application/json' },
|
|
1069
|
+
body: JSON.stringify({ hook_event_name: 'TurnComplete', session_id: event['thread-id'] ?? null, last_assistant_message: event['last-assistant-message'] ?? '', input }),
|
|
1070
|
+
signal: AbortSignal.timeout(3000),
|
|
1071
|
+
}).catch(() => {});
|
|
1072
|
+
}
|
|
1073
|
+
`;
|
|
1074
|
+
var CODEX_HOOK_SOURCE = String.raw`// cubefarm: Codex's hooks. Tells the office what Codex is doing; never changes what it does.
|
|
1075
|
+
const url = process.env.CUBEFARM_HOOK_URL;
|
|
1076
|
+
const chunks = [];
|
|
1077
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
1078
|
+
process.stdin.on('end', async () => {
|
|
1079
|
+
try {
|
|
1080
|
+
if (url) await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: Buffer.concat(chunks), signal: AbortSignal.timeout(5000) });
|
|
1081
|
+
} catch {}
|
|
1082
|
+
process.stdout.write('{}');
|
|
1083
|
+
});
|
|
1084
|
+
`;
|
|
1085
|
+
var OPENCODE_PLUGIN_SOURCE = String.raw`// cubefarm: tells the office when OpenCode's session goes idle.
|
|
1086
|
+
export default async function CubefarmPlugin({ client } = {}) {
|
|
1087
|
+
const url = process.env.CUBEFARM_NOTIFY_URL;
|
|
1088
|
+
if (!url) return {};
|
|
1089
|
+
let root;
|
|
1090
|
+
const post = (body) =>
|
|
1091
|
+
fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(3000) }).catch(() => {});
|
|
1092
|
+
const lastReply = async (id) => {
|
|
1093
|
+
try {
|
|
1094
|
+
const res = await client.session.messages({ path: { id } });
|
|
1095
|
+
const reply = (res?.data ?? []).filter((m) => m?.info?.role === 'assistant').pop();
|
|
1096
|
+
return (reply?.parts ?? []).filter((p) => p?.type === 'text').map((p) => p.text).join('\n').trim();
|
|
1097
|
+
} catch {
|
|
1098
|
+
return '';
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
return {
|
|
1102
|
+
event: async ({ event }) => {
|
|
1103
|
+
const props = event?.properties ?? {};
|
|
1104
|
+
if (event?.type === 'session.created' && !props.info?.parentID && !root) root = props.info?.id;
|
|
1105
|
+
const sessionID = props.sessionID ?? props.info?.id;
|
|
1106
|
+
if (event?.type === 'session.idle' && (!root || sessionID === root)) {
|
|
1107
|
+
post({ hook_event_name: 'TurnComplete', session_id: sessionID ?? null, last_assistant_message: sessionID ? await lastReply(sessionID) : '' });
|
|
1108
|
+
}
|
|
1109
|
+
if (event?.type === 'session.error') post({ hook_event_name: 'TurnError', session_id: sessionID ?? null, error: String(props.error?.data?.message ?? props.error?.name ?? 'error') });
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
`;
|
|
1114
|
+
|
|
1115
|
+
// server/agentRunner.ts
|
|
1116
|
+
var InputQueue = class {
|
|
1117
|
+
items = [];
|
|
1118
|
+
waiters = [];
|
|
1119
|
+
closed = false;
|
|
1120
|
+
push(text) {
|
|
1121
|
+
if (this.closed) return;
|
|
1122
|
+
const msg = { type: "user", message: { role: "user", content: text }, parent_tool_use_id: null };
|
|
1123
|
+
const waiter = this.waiters.shift();
|
|
1124
|
+
if (waiter) waiter({ value: msg, done: false });
|
|
1125
|
+
else this.items.push(msg);
|
|
1126
|
+
}
|
|
1127
|
+
close() {
|
|
1128
|
+
this.closed = true;
|
|
1129
|
+
for (const w of this.waiters.splice(0)) w({ value: void 0, done: true });
|
|
1130
|
+
}
|
|
1131
|
+
[Symbol.asyncIterator]() {
|
|
1132
|
+
return {
|
|
1133
|
+
next: () => {
|
|
1134
|
+
const item = this.items.shift();
|
|
1135
|
+
if (item) return Promise.resolve({ value: item, done: false });
|
|
1136
|
+
if (this.closed) return Promise.resolve({ value: void 0, done: true });
|
|
1137
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
var allowAll = async (_toolName, input) => ({ behavior: "allow", updatedInput: input });
|
|
1143
|
+
function screenshotFile(cwd, text) {
|
|
1144
|
+
const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
|
|
1145
|
+
if (!file) return null;
|
|
1146
|
+
try {
|
|
1147
|
+
return { data: fs5.readFileSync(path5.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
|
|
1148
|
+
} catch {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
var IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg" };
|
|
1153
|
+
function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
1154
|
+
let names;
|
|
1155
|
+
try {
|
|
1156
|
+
names = fs5.readdirSync(dir);
|
|
1157
|
+
} catch {
|
|
1158
|
+
return [];
|
|
1159
|
+
}
|
|
1160
|
+
const found2 = [];
|
|
1161
|
+
for (const name of names.sort()) {
|
|
1162
|
+
const mime = IMAGE_MIME[path5.extname(name).toLowerCase()];
|
|
1163
|
+
const file = path5.join(dir, name);
|
|
1164
|
+
if (!mime || seen.has(name)) continue;
|
|
1165
|
+
try {
|
|
1166
|
+
if (now2 - fs5.statSync(file).mtimeMs < 1e3) continue;
|
|
1167
|
+
} catch {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
found2.push({ name, file, mime });
|
|
1171
|
+
}
|
|
1172
|
+
return found2;
|
|
1173
|
+
}
|
|
1174
|
+
var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
|
|
1175
|
+
function rel(cwd, p) {
|
|
1176
|
+
const s2 = String(p ?? "");
|
|
1177
|
+
if (!s2) return "";
|
|
1178
|
+
const r = path5.relative(cwd, path5.resolve(cwd, s2));
|
|
1179
|
+
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s2;
|
|
1180
|
+
}
|
|
1181
|
+
var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1182
|
+
function tidyPaths(cwd, text) {
|
|
1183
|
+
let out = text;
|
|
1184
|
+
for (const v of /* @__PURE__ */ new Set([cwd, cwd.replaceAll("\\", "/")])) {
|
|
1185
|
+
out = out.replace(new RegExp(`^cd\\s+"?${escapeRe(v)}"?\\s*(&&|;)\\s*`, "i"), "");
|
|
1186
|
+
out = out.replace(new RegExp(`${escapeRe(v)}[\\\\/]?`, "gi"), "./");
|
|
1187
|
+
}
|
|
1188
|
+
return out;
|
|
1189
|
+
}
|
|
1190
|
+
function describeTool(cwd, name, input) {
|
|
1191
|
+
switch (name) {
|
|
1192
|
+
case "Bash":
|
|
1193
|
+
case "PowerShell":
|
|
1194
|
+
return `$ ${clip(tidyPaths(cwd, String(input.command ?? "")).split("\n")[0], 220)}`;
|
|
1195
|
+
case "Read":
|
|
1196
|
+
return `Read ${rel(cwd, input.file_path)}`;
|
|
1197
|
+
case "Write":
|
|
1198
|
+
return `Write ${rel(cwd, input.file_path)}`;
|
|
1199
|
+
case "Edit":
|
|
1200
|
+
case "MultiEdit":
|
|
1201
|
+
return `Edit ${rel(cwd, input.file_path)}`;
|
|
1202
|
+
case "Glob":
|
|
1203
|
+
return `Glob ${input.pattern}`;
|
|
1204
|
+
case "Grep":
|
|
1205
|
+
return `Grep "${clip(String(input.pattern ?? ""), 80)}"`;
|
|
1206
|
+
case "WebFetch":
|
|
1207
|
+
return `Fetch ${input.url}`;
|
|
1208
|
+
case "WebSearch":
|
|
1209
|
+
return `Search "${input.query}"`;
|
|
1210
|
+
case "Task":
|
|
1211
|
+
case "Agent":
|
|
1212
|
+
return `Subagent: ${input.description ?? ""}`;
|
|
1213
|
+
case "TodoWrite":
|
|
1214
|
+
return "Update todo list";
|
|
1215
|
+
case "ToolSearch":
|
|
1216
|
+
return `Load tools ${String(input.query ?? "").replace(/^select:/, "")}`;
|
|
1217
|
+
case "apply_patch": {
|
|
1218
|
+
const files = [...String(input.command ?? "").matchAll(/^\*\*\* (Add|Update|Delete) File: (.+)$/gm)].map((m) => `${m[1] === "Update" ? "" : `${m[1].toLowerCase()} `}${rel(cwd, m[2].trim())}`);
|
|
1219
|
+
return `Edit ${clip(files.join(", ") || "files", 200)}`;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (name.startsWith("mcp__playwright__")) {
|
|
1223
|
+
const action = name.replace("mcp__playwright__browser_", "").replace("mcp__playwright__", "");
|
|
1224
|
+
const detail = input.url ?? input.element ?? input.text ?? "";
|
|
1225
|
+
return `\u{1F310} ${action}${detail ? ` ${clip(String(detail), 120)}` : ""}`;
|
|
1226
|
+
}
|
|
1227
|
+
if (name.startsWith("mcp__office__")) return describeOfficeTool(name.slice("mcp__office__".length), input);
|
|
1228
|
+
return `${name}(${clip(JSON.stringify(input), 140)})`;
|
|
1229
|
+
}
|
|
1230
|
+
function describeOfficeTool(action, input) {
|
|
1231
|
+
const floor = input.floor != null ? ` \u2192 floor ${input.floor}` : "";
|
|
1232
|
+
switch (action) {
|
|
1233
|
+
case "company_status":
|
|
1234
|
+
return "\u{1F3E2} company_status";
|
|
1235
|
+
case "agent_detail":
|
|
1236
|
+
return `\u{1F50E} agent_detail ${String(input.agent_id ?? "")}`;
|
|
1237
|
+
case "set_floor_profile":
|
|
890
1238
|
return `\u{1F5C2}\uFE0F set_floor_profile${floor}${input.summary ? `: ${clip(String(input.summary), 90)}` : ""}`;
|
|
891
1239
|
case "update_job":
|
|
892
1240
|
return `\u{1FAAA} update_job ${input.title ? `"${clip(String(input.title), 60)}"` : String(input.agent_id ?? "")}`;
|
|
@@ -962,7 +1310,7 @@ function startSession(opts, callbacks) {
|
|
|
962
1310
|
if (problem) cb.log([{ kind: "error", text: problem }]);
|
|
963
1311
|
}
|
|
964
1312
|
if (opts.office) mcpServers.office = opts.office.server;
|
|
965
|
-
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"];
|
|
1313
|
+
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", ...CROSS_TURN_TOOLS];
|
|
966
1314
|
if (opts.role === "ceo") disallowedTools.push("Bash", "PowerShell", "NotebookEdit");
|
|
967
1315
|
const options = {
|
|
968
1316
|
cwd: opts.cwd,
|
|
@@ -1082,333 +1430,60 @@ function startSession(opts, callbacks) {
|
|
|
1082
1430
|
if (name.startsWith("mcp__playwright__")) {
|
|
1083
1431
|
const url = text.match(/Page URL:\s*(\S+)/);
|
|
1084
1432
|
if (url) cb.browserUrl(url[1]);
|
|
1085
|
-
}
|
|
1086
|
-
if (name === "TodoWrite") continue;
|
|
1087
|
-
if (block.is_error) out.push({ kind: "error", text: ` \u23BF ${clip(tidyPaths(opts.cwd, text).split("\n")[0] || "Error", 200)}` });
|
|
1088
|
-
else out.push(...summariseResult(opts.cwd, name, text));
|
|
1089
|
-
}
|
|
1090
|
-
cb.tool(null);
|
|
1091
|
-
if (out.length) cb.log(out);
|
|
1092
|
-
break;
|
|
1093
|
-
}
|
|
1094
|
-
case "result": {
|
|
1095
|
-
lastCost = msg.total_cost_usd;
|
|
1096
|
-
lastTurns = msg.num_turns;
|
|
1097
|
-
if (msg.subtype === "success" && !msg.is_error && msg.num_turns === 0) {
|
|
1098
|
-
emptyTurn = setTimeout(() => answered(msg), 3e4);
|
|
1099
|
-
break;
|
|
1100
|
-
}
|
|
1101
|
-
if (msg.subtype === "success" && !msg.is_error && msg.result.trim()) cb.turn?.(msg.result.trim());
|
|
1102
|
-
answered(msg);
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
};
|
|
1107
|
-
(async () => {
|
|
1108
|
-
try {
|
|
1109
|
-
for await (const msg of query({ prompt: input, options })) handle(msg);
|
|
1110
|
-
if (pendingTurns > 0) {
|
|
1111
|
-
cb.finished({ ok: false, text: "", costUsd: lastCost, turns: lastTurns, errors: [stopped ? "Stopped by manager" : "Session ended unexpectedly"] });
|
|
1112
|
-
}
|
|
1113
|
-
} catch (err) {
|
|
1114
|
-
input.close();
|
|
1115
|
-
const message = stopped ? "Stopped by manager" : err instanceof Error ? err.message : String(err);
|
|
1116
|
-
cb.finished({ ok: false, text: "", costUsd: lastCost, turns: lastTurns, errors: [message] });
|
|
1117
|
-
}
|
|
1118
|
-
})();
|
|
1119
|
-
return {
|
|
1120
|
-
send(text) {
|
|
1121
|
-
pendingTurns += 1;
|
|
1122
|
-
input.push(text);
|
|
1123
|
-
},
|
|
1124
|
-
stop() {
|
|
1125
|
-
stopped = true;
|
|
1126
|
-
clearTimeout(emptyTurn);
|
|
1127
|
-
input.close();
|
|
1128
|
-
abort.abort();
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// server/cliRunner.ts
|
|
1134
|
-
import crypto3 from "node:crypto";
|
|
1135
|
-
import fs7 from "node:fs";
|
|
1136
|
-
import path8 from "node:path";
|
|
1137
|
-
import { pathToFileURL } from "node:url";
|
|
1138
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1139
|
-
|
|
1140
|
-
// server/clis.ts
|
|
1141
|
-
import fs5 from "node:fs";
|
|
1142
|
-
import { createRequire as createRequire2 } from "node:module";
|
|
1143
|
-
import path5 from "node:path";
|
|
1144
|
-
var CLIS = [
|
|
1145
|
-
{ id: "claude", label: "Claude Code", command: "claude", integrated: true },
|
|
1146
|
-
{ id: "codex", label: "Codex", command: "codex", integrated: false },
|
|
1147
|
-
{ id: "opencode", label: "OpenCode", command: "opencode", integrated: false }
|
|
1148
|
-
];
|
|
1149
|
-
var isCli = (v) => CLIS.some((c) => c.id === v);
|
|
1150
|
-
var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
|
|
1151
|
-
var WIN = process.platform === "win32";
|
|
1152
|
-
function resolveCommand(name, env = process.env) {
|
|
1153
|
-
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
1154
|
-
const dirs = (env[key] ?? "").split(path5.delimiter).filter(Boolean);
|
|
1155
|
-
const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
|
|
1156
|
-
for (const dir of dirs) {
|
|
1157
|
-
for (const ext of exts) {
|
|
1158
|
-
const file = path5.join(dir, name + ext.toLowerCase());
|
|
1159
|
-
try {
|
|
1160
|
-
const st = fs5.statSync(file);
|
|
1161
|
-
if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
|
|
1162
|
-
} catch {
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
return null;
|
|
1167
|
-
}
|
|
1168
|
-
function unwrapCmdShim(shimPath, text) {
|
|
1169
|
-
const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
|
|
1170
|
-
if (!line) return null;
|
|
1171
|
-
const dir = path5.dirname(shimPath);
|
|
1172
|
-
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path5.join(dir, m[1]));
|
|
1173
|
-
const target = targets[targets.length - 1];
|
|
1174
|
-
if (!target) return null;
|
|
1175
|
-
if (/\.exe$/i.test(target)) return { file: target, args: [] };
|
|
1176
|
-
if (!/\.[cm]?js$/i.test(target)) return null;
|
|
1177
|
-
const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
|
|
1178
|
-
const nodeFlags = line.includes('"%_prog%"') ? between.trim().split(/\s+/).filter((f) => f.startsWith("--")) : [];
|
|
1179
|
-
const localNode = path5.join(dir, "node.exe");
|
|
1180
|
-
return { file: fs5.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
|
|
1181
|
-
}
|
|
1182
|
-
var bundled;
|
|
1183
|
-
function bundledClaude() {
|
|
1184
|
-
if (bundled !== void 0) return bundled;
|
|
1185
|
-
bundled = null;
|
|
1186
|
-
let sdk;
|
|
1187
|
-
try {
|
|
1188
|
-
sdk = createRequire2(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
1189
|
-
} catch {
|
|
1190
|
-
return null;
|
|
1191
|
-
}
|
|
1192
|
-
const { platform, arch } = process;
|
|
1193
|
-
const report = platform === "linux" ? process.report?.getReport() : void 0;
|
|
1194
|
-
const musl = platform === "linux" && !report?.header?.glibcVersionRuntime;
|
|
1195
|
-
const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
1196
|
-
for (const target of targets) {
|
|
1197
|
-
try {
|
|
1198
|
-
bundled = createRequire2(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
|
|
1199
|
-
return bundled;
|
|
1200
|
-
} catch {
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
return null;
|
|
1204
|
-
}
|
|
1205
|
-
function commandFor(id) {
|
|
1206
|
-
const bundled2 = id === "claude" ? bundledClaude() : null;
|
|
1207
|
-
if (bundled2) return { file: bundled2, args: [] };
|
|
1208
|
-
const def = CLIS.find((c) => c.id === id);
|
|
1209
|
-
const found2 = def && resolveCommand(def.command);
|
|
1210
|
-
if (!found2) return null;
|
|
1211
|
-
if (WIN && /\.(cmd|bat)$/i.test(found2)) {
|
|
1212
|
-
try {
|
|
1213
|
-
return unwrapCmdShim(found2, fs5.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
|
|
1214
|
-
} catch {
|
|
1215
|
-
return { file: found2, args: [] };
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
return { file: found2, args: [] };
|
|
1219
|
-
}
|
|
1220
|
-
async function detectClis() {
|
|
1221
|
-
return Promise.all(
|
|
1222
|
-
CLIS.map(async (c) => {
|
|
1223
|
-
const cmd = commandFor(c.id);
|
|
1224
|
-
const version = cmd ? await run(cmd.file, [...cmd.args, "--version"], { timeoutMs: 2e4 }).catch(() => null) : null;
|
|
1225
|
-
return { id: c.id, label: c.label, installed: !!cmd, version: version?.split(/\r?\n/)[0].trim().slice(0, 60) || null, integrated: c.integrated };
|
|
1226
|
-
})
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
|
|
1230
|
-
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
|
|
1231
|
-
function codexHookCommand(node, script, win = WIN) {
|
|
1232
|
-
return win ? `node "${script}"` : [node, script].map((s2) => `'${s2.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
1233
|
-
}
|
|
1234
|
-
function launchArgs(id, ctx) {
|
|
1235
|
-
switch (id) {
|
|
1236
|
-
case "claude": {
|
|
1237
|
-
const disallowed = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", ...ctx.role === "ceo" ? ["Bash", "PowerShell", "NotebookEdit"] : []];
|
|
1238
|
-
const args = [
|
|
1239
|
-
...ctx.resumeId ? ["--resume", ctx.resumeId] : ["--session-id", ctx.sessionId],
|
|
1240
|
-
...ctx.model ? ["--model", ctx.model] : [],
|
|
1241
|
-
...ctx.effort ? ["--effort", ctx.effort] : [],
|
|
1242
|
-
// The office's PreToolUse hook answers every permission question (allow, or deny with an office rule).
|
|
1243
|
-
"--permission-mode",
|
|
1244
|
-
"acceptEdits",
|
|
1245
|
-
// On top of the manager's own setup (settings, skills, plugins, MCP servers): the office's hooks and servers.
|
|
1246
|
-
"--settings",
|
|
1247
|
-
ctx.files.settings,
|
|
1248
|
-
...ctx.files.mcp ? ["--mcp-config", ctx.files.mcp] : [],
|
|
1249
|
-
"--append-system-prompt-file",
|
|
1250
|
-
ctx.files.system,
|
|
1251
|
-
"--name",
|
|
1252
|
-
ctx.name,
|
|
1253
|
-
...ctx.additionalDirectories.flatMap((d) => ["--add-dir", d]),
|
|
1254
|
-
"--disallowedTools",
|
|
1255
|
-
...disallowed,
|
|
1256
|
-
"--",
|
|
1257
|
-
ctx.prompt
|
|
1258
|
-
];
|
|
1259
|
-
return { args, env: { DISABLE_AUTOUPDATER: "1" } };
|
|
1260
|
-
}
|
|
1261
|
-
case "codex": {
|
|
1262
|
-
const toml = (s2) => JSON.stringify(s2);
|
|
1263
|
-
const args = [
|
|
1264
|
-
"--no-alt-screen",
|
|
1265
|
-
"-c",
|
|
1266
|
-
`notify=[${[process.execPath, ctx.notify.script, ctx.notify.url].map(toml).join(",")}]`,
|
|
1267
|
-
"-c",
|
|
1268
|
-
`developer_instructions=${toml(ctx.systemAppend)}`,
|
|
1269
|
-
...ctx.model ? ["-m", ctx.model] : [],
|
|
1270
|
-
...ctx.effort ? ["-c", `model_reasoning_effort=${toml(EFFORT_CODEX[ctx.effort])}`] : [],
|
|
1271
|
-
...ctx.browser ? ["-c", `mcp_servers.playwright={command=${toml(ctx.browser.command)},args=[${ctx.browser.args.map(toml).join(",")}]}`] : [],
|
|
1272
|
-
...CODEX_HOOK_EVENTS.flatMap((e) => ["-c", `hooks.${e}=[{hooks=[{type="command",command=${toml(codexHookCommand(process.execPath, ctx.codexHook))},timeout=10}]}]`]),
|
|
1273
|
-
// Like the manager's own Codex, but it can't stop to ask: no approvals and no sandbox (its sandbox can't reach
|
|
1274
|
-
// the credential store, so git and gh fail in it).
|
|
1275
|
-
"--dangerously-bypass-approvals-and-sandbox"
|
|
1276
|
-
];
|
|
1277
|
-
return { args: ctx.resumeId ? ["resume", ...args, ctx.resumeId, ctx.prompt] : [...args, "--", ctx.prompt], env: { CUBEFARM_HOOK_URL: ctx.notify.url } };
|
|
1278
|
-
}
|
|
1279
|
-
case "opencode": {
|
|
1280
|
-
const config = {
|
|
1281
|
-
plugin: [ctx.plugin],
|
|
1282
|
-
instructions: [ctx.files.system],
|
|
1283
|
-
autoupdate: false,
|
|
1284
|
-
// several agents starting at once must not each reinstall it
|
|
1285
|
-
permission: { edit: "allow", bash: "allow", webfetch: "allow" },
|
|
1286
|
-
// it can't stop to ask either
|
|
1287
|
-
...ctx.browser ? { mcp: { playwright: { type: "local", command: [ctx.browser.command, ...ctx.browser.args], enabled: true } } } : {}
|
|
1288
|
-
};
|
|
1289
|
-
const args = [
|
|
1290
|
-
"--auto",
|
|
1291
|
-
...ctx.model ? ["--model", ctx.model] : [],
|
|
1292
|
-
...ctx.resumeId ? ["--session", ctx.resumeId] : [],
|
|
1293
|
-
"--prompt",
|
|
1294
|
-
ctx.prompt
|
|
1295
|
-
];
|
|
1296
|
-
return { args, env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(config), CUBEFARM_NOTIFY_URL: ctx.notify.url } };
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
function oneAtATime() {
|
|
1301
|
-
const tails = /* @__PURE__ */ new Map();
|
|
1302
|
-
return (key, step) => {
|
|
1303
|
-
const next = (tails.get(key) ?? Promise.resolve()).then(step).then(
|
|
1304
|
-
() => void 0,
|
|
1305
|
-
() => void 0
|
|
1306
|
-
);
|
|
1307
|
-
tails.set(key, next);
|
|
1308
|
-
void next.then(() => tails.get(key) === next && tails.delete(key));
|
|
1309
|
-
return next;
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
var threadSteps = oneAtATime();
|
|
1313
|
-
function codexThread(action, id, after) {
|
|
1314
|
-
return threadSteps(id, async () => {
|
|
1315
|
-
await after;
|
|
1316
|
-
const cmd = commandFor("codex");
|
|
1317
|
-
if (cmd) await run(cmd.file, [...cmd.args, action, id], { timeoutMs: 3e4 });
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
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;
|
|
1321
|
-
function trustKey(screen) {
|
|
1322
|
-
if (!TRUST_PROMPT.test(screen)) return null;
|
|
1323
|
-
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
1324
|
-
if (/^(yes|trust|continue|proceed)\b/i.test(selected.trim())) return "enter";
|
|
1325
|
-
if (/^(no|exit|quit|cancel)\b/i.test(selected.trim())) return "down";
|
|
1326
|
-
return null;
|
|
1327
|
-
}
|
|
1328
|
-
function hookReviewKey(screen) {
|
|
1329
|
-
if (!/Hooks need review/i.test(screen)) return null;
|
|
1330
|
-
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
1331
|
-
return /^continue without trusting/i.test(selected.trim()) ? "enter" : "down";
|
|
1332
|
-
}
|
|
1333
|
-
function interruptions(screen) {
|
|
1334
|
-
return screen.split("\n").filter((l) => /\binterrupted\b/i.test(l)).length;
|
|
1335
|
-
}
|
|
1336
|
-
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.
|
|
1337
|
-
const url = process.argv[2];
|
|
1338
|
-
let body = '';
|
|
1339
|
-
process.stdin.on('data', (c) => (body += c));
|
|
1340
|
-
process.stdin.on('end', async () => {
|
|
1341
|
-
try {
|
|
1342
|
-
const status = JSON.parse(body);
|
|
1343
|
-
const res = await fetch(url, {
|
|
1344
|
-
method: 'POST',
|
|
1345
|
-
headers: { 'content-type': 'application/json' },
|
|
1346
|
-
body: JSON.stringify({ hook_event_name: 'StatusLine', session_id: status.session_id, cost: status.cost, rate_limits: status.rate_limits }),
|
|
1347
|
-
signal: AbortSignal.timeout(2000),
|
|
1348
|
-
});
|
|
1349
|
-
const out = await res.json();
|
|
1350
|
-
process.stdout.write(String(out.statusLine ?? ''));
|
|
1351
|
-
} catch {
|
|
1352
|
-
process.stdout.write('cubefarm');
|
|
1353
|
-
}
|
|
1354
|
-
});
|
|
1355
|
-
`;
|
|
1356
|
-
var NOTIFY_SOURCE = String.raw`// cubefarm: Codex's notify program. Tells the office a turn is complete.
|
|
1357
|
-
const [url, payload] = process.argv.slice(2);
|
|
1358
|
-
let event = {};
|
|
1359
|
-
try {
|
|
1360
|
-
event = JSON.parse(payload ?? '{}');
|
|
1361
|
-
} catch {}
|
|
1362
|
-
if (event.type === 'agent-turn-complete') {
|
|
1363
|
-
const input = String((event['input-messages'] ?? [])[0] ?? '').slice(0, 300);
|
|
1364
|
-
fetch(url, {
|
|
1365
|
-
method: 'POST',
|
|
1366
|
-
headers: { 'content-type': 'application/json' },
|
|
1367
|
-
body: JSON.stringify({ hook_event_name: 'TurnComplete', session_id: event['thread-id'] ?? null, last_assistant_message: event['last-assistant-message'] ?? '', input }),
|
|
1368
|
-
signal: AbortSignal.timeout(3000),
|
|
1369
|
-
}).catch(() => {});
|
|
1370
|
-
}
|
|
1371
|
-
`;
|
|
1372
|
-
var CODEX_HOOK_SOURCE = String.raw`// cubefarm: Codex's hooks. Tells the office what Codex is doing; never changes what it does.
|
|
1373
|
-
const url = process.env.CUBEFARM_HOOK_URL;
|
|
1374
|
-
const chunks = [];
|
|
1375
|
-
process.stdin.on('data', (c) => chunks.push(c));
|
|
1376
|
-
process.stdin.on('end', async () => {
|
|
1377
|
-
try {
|
|
1378
|
-
if (url) await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: Buffer.concat(chunks), signal: AbortSignal.timeout(5000) });
|
|
1379
|
-
} catch {}
|
|
1380
|
-
process.stdout.write('{}');
|
|
1381
|
-
});
|
|
1382
|
-
`;
|
|
1383
|
-
var OPENCODE_PLUGIN_SOURCE = String.raw`// cubefarm: tells the office when OpenCode's session goes idle.
|
|
1384
|
-
export default async function CubefarmPlugin({ client } = {}) {
|
|
1385
|
-
const url = process.env.CUBEFARM_NOTIFY_URL;
|
|
1386
|
-
if (!url) return {};
|
|
1387
|
-
let root;
|
|
1388
|
-
const post = (body) =>
|
|
1389
|
-
fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(3000) }).catch(() => {});
|
|
1390
|
-
const lastReply = async (id) => {
|
|
1391
|
-
try {
|
|
1392
|
-
const res = await client.session.messages({ path: { id } });
|
|
1393
|
-
const reply = (res?.data ?? []).filter((m) => m?.info?.role === 'assistant').pop();
|
|
1394
|
-
return (reply?.parts ?? []).filter((p) => p?.type === 'text').map((p) => p.text).join('\n').trim();
|
|
1395
|
-
} catch {
|
|
1396
|
-
return '';
|
|
1433
|
+
}
|
|
1434
|
+
if (name === "TodoWrite") continue;
|
|
1435
|
+
if (block.is_error) out.push({ kind: "error", text: ` \u23BF ${clip(tidyPaths(opts.cwd, text).split("\n")[0] || "Error", 200)}` });
|
|
1436
|
+
else out.push(...summariseResult(opts.cwd, name, text));
|
|
1437
|
+
}
|
|
1438
|
+
cb.tool(null);
|
|
1439
|
+
if (out.length) cb.log(out);
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
case "result": {
|
|
1443
|
+
lastCost = msg.total_cost_usd;
|
|
1444
|
+
lastTurns = msg.num_turns;
|
|
1445
|
+
if (msg.subtype === "success" && !msg.is_error && msg.num_turns === 0) {
|
|
1446
|
+
emptyTurn = setTimeout(() => answered(msg), 3e4);
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
if (msg.subtype === "success" && !msg.is_error && msg.result.trim()) cb.turn?.(msg.result.trim());
|
|
1450
|
+
answered(msg);
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1397
1453
|
}
|
|
1398
1454
|
};
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
if (event?.type === 'session.idle' && (!root || sessionID === root)) {
|
|
1405
|
-
post({ hook_event_name: 'TurnComplete', session_id: sessionID ?? null, last_assistant_message: sessionID ? await lastReply(sessionID) : '' });
|
|
1455
|
+
(async () => {
|
|
1456
|
+
try {
|
|
1457
|
+
for await (const msg of query({ prompt: input, options })) handle(msg);
|
|
1458
|
+
if (pendingTurns > 0) {
|
|
1459
|
+
cb.finished({ ok: false, text: "", costUsd: lastCost, turns: lastTurns, errors: [stopped ? "Stopped by manager" : "Session ended unexpectedly"] });
|
|
1406
1460
|
}
|
|
1407
|
-
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
input.close();
|
|
1463
|
+
const message = stopped ? "Stopped by manager" : err instanceof Error ? err.message : String(err);
|
|
1464
|
+
cb.finished({ ok: false, text: "", costUsd: lastCost, turns: lastTurns, errors: [message] });
|
|
1465
|
+
}
|
|
1466
|
+
})();
|
|
1467
|
+
return {
|
|
1468
|
+
send(text) {
|
|
1469
|
+
pendingTurns += 1;
|
|
1470
|
+
input.push(text);
|
|
1408
1471
|
},
|
|
1472
|
+
stop() {
|
|
1473
|
+
stopped = true;
|
|
1474
|
+
clearTimeout(emptyTurn);
|
|
1475
|
+
input.close();
|
|
1476
|
+
abort.abort();
|
|
1477
|
+
}
|
|
1409
1478
|
};
|
|
1410
1479
|
}
|
|
1411
|
-
|
|
1480
|
+
|
|
1481
|
+
// server/cliRunner.ts
|
|
1482
|
+
import crypto3 from "node:crypto";
|
|
1483
|
+
import fs7 from "node:fs";
|
|
1484
|
+
import path8 from "node:path";
|
|
1485
|
+
import { pathToFileURL } from "node:url";
|
|
1486
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1412
1487
|
|
|
1413
1488
|
// server/ptyClient.ts
|
|
1414
1489
|
import crypto2 from "node:crypto";
|
|
@@ -2924,6 +2999,7 @@ var realBackend = {
|
|
|
2924
2999
|
installDeps: (dir, cb) => installDesk(dir, cb),
|
|
2925
3000
|
removeDesk,
|
|
2926
3001
|
sweepDesks,
|
|
3002
|
+
trimDesk,
|
|
2927
3003
|
releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
|
|
2928
3004
|
startSession: (opts, cb) => opts.terminal ? startCliSession(opts, cb) : startSession(opts, cb),
|
|
2929
3005
|
terminals: terminalsAvailable,
|
|
@@ -3422,6 +3498,12 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
3422
3498
|
},
|
|
3423
3499
|
removeDesk: async () => void 0,
|
|
3424
3500
|
sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
|
|
3501
|
+
// A made-up install and build of 300-900 MB, so the setting and the phone message can be seen.
|
|
3502
|
+
trimDesk: async (_fullName, _slug, stillIdle) => {
|
|
3503
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3504
|
+
if (!stillIdle()) return null;
|
|
3505
|
+
return { freed: Math.round((300 + Math.random() * 600) * 2 ** 20), removed: ["node_modules", "dist"], skipped: [] };
|
|
3506
|
+
},
|
|
3425
3507
|
releaseDesk: async () => void 0,
|
|
3426
3508
|
startSession: (opts, cb) => {
|
|
3427
3509
|
if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
|
|
@@ -4021,6 +4103,101 @@ function holdUps(issues) {
|
|
|
4021
4103
|
return out;
|
|
4022
4104
|
}
|
|
4023
4105
|
|
|
4106
|
+
// server/prompts.ts
|
|
4107
|
+
var PLACEHOLDERS = {
|
|
4108
|
+
worktree: "<your worktree>",
|
|
4109
|
+
issue: "<issue>",
|
|
4110
|
+
pr: "<PR>",
|
|
4111
|
+
prTitle: "<PR title>",
|
|
4112
|
+
prUrl: "<PR URL>",
|
|
4113
|
+
branch: "<branch>"
|
|
4114
|
+
};
|
|
4115
|
+
var ONE_TURN = "Your session ends when your turn ends: background tasks and notifications won't wake you. Never end a turn to wait. Wait in the foreground with a timeout, then finish.";
|
|
4116
|
+
var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
|
|
4117
|
+
var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
|
|
4118
|
+
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing, depsLine }) {
|
|
4119
|
+
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
4120
|
+
const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
|
|
4121
|
+
return [
|
|
4122
|
+
`You are ${a.name}, ${a.title ? `the team's ${a.title},` : "a software engineer"} on an autonomous agent team ("cubefarm"). Several teammates work in parallel on other issues of the same repository, each in their own git worktree. Nobody is watching live to answer questions, so make sensible decisions yourself and record assumptions in the PR description. The manager may occasionally send you messages; follow their instructions.`,
|
|
4123
|
+
`Every pull request is reviewed and tested by a QA teammate. ${repo.autoMerge ? "Once they sign off and GitHub's checks pass, the office merges it by itself" : "Once they sign off, the manager merges it"}. If they find problems, or checks fail, or it conflicts with the default branch, you will get the details; fix them on the same branch.`,
|
|
4124
|
+
a.brief ? `
|
|
4125
|
+
Your job description:
|
|
4126
|
+
${a.brief}` : "",
|
|
4127
|
+
"",
|
|
4128
|
+
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
4129
|
+
repo.summary ? `Project: ${repo.summary}` : "",
|
|
4130
|
+
repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
|
|
4131
|
+
`Your worktree: ${cwd}`,
|
|
4132
|
+
depsLine ?? "",
|
|
4133
|
+
fixing ? `You are fixing pull request #${fixing.pr}. Its code is checked out on local branch ${branch}; push fixes with: ${push}. Do not open a new pull request.` : `Your branch: ${branch} (already checked out, created from origin/${repo.defaultBranch})`,
|
|
4134
|
+
links.length ? `Related repositories you may read for context (do not modify them):
|
|
4135
|
+
${links.join("\n")}` : "",
|
|
4136
|
+
"",
|
|
4137
|
+
"Workflow:",
|
|
4138
|
+
"1. Read the issue and explore the relevant code before changing anything.",
|
|
4139
|
+
"2. Implement the change with focused commits and clear messages.",
|
|
4140
|
+
"3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed (e.g. npm install when node_modules is missing).",
|
|
4141
|
+
repo.browserTesting ? `4. If the project has a web UI, start its dev server in the background on port ${port} (reserved for you, so you don't collide with teammates), then check your change with the Playwright browser tools (mcp__playwright__browser_navigate, browser_snapshot, browser_click, browser_take_screenshot). Stop the dev server when you're done.` : "4. Verify the behaviour you changed as directly as you can.",
|
|
4142
|
+
`5. Push: ${push}`,
|
|
4143
|
+
fixing ? "6. Reply with a short summary of what you fixed." : `6. Open a pull request with the GitHub CLI: gh pr create --base ${repo.defaultBranch} --head ${branch} --title "<concise title>" --body "<what changed, how you verified it, assumptions>". The body must contain "Closes #<issue number>".`,
|
|
4144
|
+
fixing ? "" : "7. End your final message with the pull request URL on its own line.",
|
|
4145
|
+
"",
|
|
4146
|
+
"Rules: never push to the default branch, never force-push, never merge pull requests yourself (the office merges them once QA and the checks pass), and never edit files outside your worktree. If you cannot finish, open a draft PR (gh pr create --draft) explaining what is left and why.",
|
|
4147
|
+
ONE_TURN
|
|
4148
|
+
].filter((l) => l !== "").join("\n");
|
|
4149
|
+
}
|
|
4150
|
+
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, depsLine, testStep: testStep2 }) {
|
|
4151
|
+
return [
|
|
4152
|
+
`You are ${a.name}, ${a.title ? `the team's ${a.title},` : "a QA engineer"} on an autonomous agent team ("cubefarm"). Developers open pull requests; you review and independently verify each one before it is merged. Your sign-off is the review: ${repo.autoMerge ? "on this floor a PR you pass merges by itself as soon as GitHub's checks are green, so nobody else reads the code after you. " : ""}Be thorough and skeptical, but fair: fail a PR only for real problems (broken behaviour, failing tests or build, the issue's requirements not met, obvious regressions), not for style preferences.`,
|
|
4153
|
+
...a.brief ? ["", `Your job description:
|
|
4154
|
+
${a.brief}`] : [],
|
|
4155
|
+
...a.role === "dev" ? ["", "You're a developer covering for the QA lab while its testers are busy. You didn't write this pull request: test it as an independent QA engineer would."] : [],
|
|
4156
|
+
"",
|
|
4157
|
+
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
4158
|
+
...repo.summary ? [`Project: ${repo.summary}`] : [],
|
|
4159
|
+
...repo.mission ? [`What the team is building (the manager's brief): ${repo.mission}`] : [],
|
|
4160
|
+
...repo.qaBrief ? [`What to check on this project (from the CEO):
|
|
4161
|
+
${repo.qaBrief}`] : [],
|
|
4162
|
+
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
4163
|
+
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
4164
|
+
...depsLine ? [depsLine] : [],
|
|
4165
|
+
"",
|
|
4166
|
+
"How to test:",
|
|
4167
|
+
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
4168
|
+
`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.`,
|
|
4169
|
+
testStep2 ?? "3. Install dependencies if needed (e.g. npm install when node_modules is missing), then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4170
|
+
repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${port} (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).",
|
|
4171
|
+
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
4172
|
+
"A merge conflict with the default branch is not a fail, nor is a red check unrelated to this change (a flake or an outage: say why): judge the change itself (on a throwaway merge if you need newer work) and pass it if it's good. The office sends conflicting PRs back for a merge fix, then you re-test, and re-runs a failed check before anyone fixes it.",
|
|
4173
|
+
"",
|
|
4174
|
+
"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.",
|
|
4175
|
+
ONE_TURN
|
|
4176
|
+
].join("\n");
|
|
4177
|
+
}
|
|
4178
|
+
var OFFICE = "Office instructions";
|
|
4179
|
+
var MARK = "\0job description\0";
|
|
4180
|
+
function promptParts(build, brief) {
|
|
4181
|
+
if (!brief) return [{ label: OFFICE, text: build(""), editable: false }];
|
|
4182
|
+
const [before, after] = build(MARK).split(MARK);
|
|
4183
|
+
return [
|
|
4184
|
+
{ label: OFFICE, text: before, editable: false },
|
|
4185
|
+
{ label: "Job description", text: brief, editable: true },
|
|
4186
|
+
{ label: OFFICE, text: after, editable: false }
|
|
4187
|
+
].filter((p) => p.text !== "");
|
|
4188
|
+
}
|
|
4189
|
+
var view = (kind, parts) => ({ kind, text: parts.map((p) => p.text).join(""), parts });
|
|
4190
|
+
function devPromptPreview({ slug, ...i }) {
|
|
4191
|
+
const branch = devBranch(PLACEHOLDERS.issue, slug);
|
|
4192
|
+
return view("dev", promptParts((brief) => devSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch }), i.agent.brief));
|
|
4193
|
+
}
|
|
4194
|
+
function qaPromptPreview({ slug, ...i }) {
|
|
4195
|
+
const pr = { number: PLACEHOLDERS.pr, title: PLACEHOLDERS.prTitle, headRefName: PLACEHOLDERS.branch, url: PLACEHOLDERS.prUrl };
|
|
4196
|
+
const branch = qaBranch(PLACEHOLDERS.pr, slug);
|
|
4197
|
+
return view("qa", promptParts((brief) => qaSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch, pr }), i.agent.brief));
|
|
4198
|
+
}
|
|
4199
|
+
var ceoPromptPreview = (text) => view("ceo", [{ label: OFFICE, text, editable: false }]);
|
|
4200
|
+
|
|
4024
4201
|
// server/ceo.ts
|
|
4025
4202
|
var EFFORT = z2.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
4026
4203
|
function createOfficeTools(h) {
|
|
@@ -4161,6 +4338,7 @@ function ceoSystemPrompt(o) {
|
|
|
4161
4338
|
"- Read the repositories through their clone paths with Read, Glob and Grep. They are read-only to you. You cannot run shell commands.",
|
|
4162
4339
|
`- Keep durable notes about the company in ${o.notesFile}: read it at the start, and update it at the end with decisions and anything worth remembering next time.`,
|
|
4163
4340
|
"- Change things only through the mcp__office__ tools.",
|
|
4341
|
+
`- ${ONE_TURN}`,
|
|
4164
4342
|
"- Before update_job rewrites someone's job description, read the full one with mcp__office__agent_detail and keep what still applies, especially its safety rules.",
|
|
4165
4343
|
"",
|
|
4166
4344
|
"Rules:",
|
|
@@ -4394,7 +4572,7 @@ function checksLine(i) {
|
|
|
4394
4572
|
return `GitHub checks right now: ${state}`;
|
|
4395
4573
|
}
|
|
4396
4574
|
function testStep(hasChecks) {
|
|
4397
|
-
return hasChecks ? "3. GitHub's checks run on this PR, and it merges only once they're green. Read .github/workflows to see what they cover and don't re-run that locally. Run only what you need to exercise the change (for example a build to start the app), anything the checks don't cover, or a failing check to reproduce it. A failing check is a finding: name it." : "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).";
|
|
4575
|
+
return hasChecks ? "3. GitHub's checks run on this PR, and it merges only once they're green. Read .github/workflows to see what they cover and don't re-run that locally. Run only what you need to exercise the change (for example a build to start the app), anything the checks don't cover, or a failing check to reproduce it. A failing check is a finding: name it." : "3. Install dependencies if needed (e.g. npm install when node_modules is missing), then run the project's test suite, linters, type checks and build (whichever exist).";
|
|
4398
4576
|
}
|
|
4399
4577
|
function retest(i) {
|
|
4400
4578
|
const since = i.lastTestedSha && i.lastTestedSha !== i.headSha ? i.lastTestedSha : null;
|
|
@@ -4515,6 +4693,10 @@ var Previews = class {
|
|
|
4515
4693
|
logTail: (r?.log ?? []).slice(-LOG_VIEW)
|
|
4516
4694
|
};
|
|
4517
4695
|
}
|
|
4696
|
+
/** The floor's preview is being set up or running: its worktree is in use. */
|
|
4697
|
+
active(f) {
|
|
4698
|
+
return ACTIVE.includes(this.runs.get(f.id)?.status ?? "stopped");
|
|
4699
|
+
}
|
|
4518
4700
|
/** Re-check whether a floor without a command has a package.json to fall back on. */
|
|
4519
4701
|
async refreshDefault(f) {
|
|
4520
4702
|
const has = await this.backend.previews.hasDefault(f.fullName).catch(() => true);
|
|
@@ -4639,97 +4821,13 @@ var Previews = class {
|
|
|
4639
4821
|
}
|
|
4640
4822
|
};
|
|
4641
4823
|
|
|
4642
|
-
// server/
|
|
4643
|
-
var
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
prUrl: "<PR URL>",
|
|
4649
|
-
branch: "<branch>"
|
|
4650
|
-
};
|
|
4651
|
-
var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
|
|
4652
|
-
var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
|
|
4653
|
-
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing, depsLine }) {
|
|
4654
|
-
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
4655
|
-
const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
|
|
4656
|
-
return [
|
|
4657
|
-
`You are ${a.name}, ${a.title ? `the team's ${a.title},` : "a software engineer"} on an autonomous agent team ("cubefarm"). Several teammates work in parallel on other issues of the same repository, each in their own git worktree. Nobody is watching live to answer questions, so make sensible decisions yourself and record assumptions in the PR description. The manager may occasionally send you messages; follow their instructions.`,
|
|
4658
|
-
`Every pull request is reviewed and tested by a QA teammate. ${repo.autoMerge ? "Once they sign off and GitHub's checks pass, the office merges it by itself" : "Once they sign off, the manager merges it"}. If they find problems, or checks fail, or it conflicts with the default branch, you will get the details; fix them on the same branch.`,
|
|
4659
|
-
a.brief ? `
|
|
4660
|
-
Your job description:
|
|
4661
|
-
${a.brief}` : "",
|
|
4662
|
-
"",
|
|
4663
|
-
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
4664
|
-
repo.summary ? `Project: ${repo.summary}` : "",
|
|
4665
|
-
repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
|
|
4666
|
-
`Your worktree: ${cwd}`,
|
|
4667
|
-
depsLine ?? "",
|
|
4668
|
-
fixing ? `You are fixing pull request #${fixing.pr}. Its code is checked out on local branch ${branch}; push fixes with: ${push}. Do not open a new pull request.` : `Your branch: ${branch} (already checked out, created from origin/${repo.defaultBranch})`,
|
|
4669
|
-
links.length ? `Related repositories you may read for context (do not modify them):
|
|
4670
|
-
${links.join("\n")}` : "",
|
|
4671
|
-
"",
|
|
4672
|
-
"Workflow:",
|
|
4673
|
-
"1. Read the issue and explore the relevant code before changing anything.",
|
|
4674
|
-
"2. Implement the change with focused commits and clear messages.",
|
|
4675
|
-
"3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed.",
|
|
4676
|
-
repo.browserTesting ? `4. If the project has a web UI, start its dev server in the background on port ${port} (reserved for you, so you don't collide with teammates), then check your change with the Playwright browser tools (mcp__playwright__browser_navigate, browser_snapshot, browser_click, browser_take_screenshot). Stop the dev server when you're done.` : "4. Verify the behaviour you changed as directly as you can.",
|
|
4677
|
-
`5. Push: ${push}`,
|
|
4678
|
-
fixing ? "6. Reply with a short summary of what you fixed." : `6. Open a pull request with the GitHub CLI: gh pr create --base ${repo.defaultBranch} --head ${branch} --title "<concise title>" --body "<what changed, how you verified it, assumptions>". The body must contain "Closes #<issue number>".`,
|
|
4679
|
-
fixing ? "" : "7. End your final message with the pull request URL on its own line.",
|
|
4680
|
-
"",
|
|
4681
|
-
"Rules: never push to the default branch, never force-push, never merge pull requests yourself (the office merges them once QA and the checks pass), and never edit files outside your worktree. If you cannot finish, open a draft PR (gh pr create --draft) explaining what is left and why."
|
|
4682
|
-
].filter((l) => l !== "").join("\n");
|
|
4683
|
-
}
|
|
4684
|
-
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, depsLine, testStep: testStep2 }) {
|
|
4685
|
-
return [
|
|
4686
|
-
`You are ${a.name}, ${a.title ? `the team's ${a.title},` : "a QA engineer"} on an autonomous agent team ("cubefarm"). Developers open pull requests; you review and independently verify each one before it is merged. Your sign-off is the review: ${repo.autoMerge ? "on this floor a PR you pass merges by itself as soon as GitHub's checks are green, so nobody else reads the code after you. " : ""}Be thorough and skeptical, but fair: fail a PR only for real problems (broken behaviour, failing tests or build, the issue's requirements not met, obvious regressions), not for style preferences.`,
|
|
4687
|
-
...a.brief ? ["", `Your job description:
|
|
4688
|
-
${a.brief}`] : [],
|
|
4689
|
-
...a.role === "dev" ? ["", "You're a developer covering for the QA lab while its testers are busy. You didn't write this pull request: test it as an independent QA engineer would."] : [],
|
|
4690
|
-
"",
|
|
4691
|
-
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
4692
|
-
...repo.summary ? [`Project: ${repo.summary}`] : [],
|
|
4693
|
-
...repo.mission ? [`What the team is building (the manager's brief): ${repo.mission}`] : [],
|
|
4694
|
-
...repo.qaBrief ? [`What to check on this project (from the CEO):
|
|
4695
|
-
${repo.qaBrief}`] : [],
|
|
4696
|
-
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
4697
|
-
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
4698
|
-
...depsLine ? [depsLine] : [],
|
|
4699
|
-
"",
|
|
4700
|
-
"How to test:",
|
|
4701
|
-
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
4702
|
-
`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.`,
|
|
4703
|
-
testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4704
|
-
repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${port} (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).",
|
|
4705
|
-
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
4706
|
-
"A merge conflict with the default branch is not a fail, nor is a red check unrelated to this change (a flake or an outage: say why): judge the change itself (on a throwaway merge if you need newer work) and pass it if it's good. The office sends conflicting PRs back for a merge fix, then you re-test, and re-runs a failed check before anyone fixes it.",
|
|
4707
|
-
"",
|
|
4708
|
-
"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."
|
|
4709
|
-
].join("\n");
|
|
4710
|
-
}
|
|
4711
|
-
var OFFICE = "Office instructions";
|
|
4712
|
-
var MARK = "\0job description\0";
|
|
4713
|
-
function promptParts(build, brief) {
|
|
4714
|
-
if (!brief) return [{ label: OFFICE, text: build(""), editable: false }];
|
|
4715
|
-
const [before, after] = build(MARK).split(MARK);
|
|
4716
|
-
return [
|
|
4717
|
-
{ label: OFFICE, text: before, editable: false },
|
|
4718
|
-
{ label: "Job description", text: brief, editable: true },
|
|
4719
|
-
{ label: OFFICE, text: after, editable: false }
|
|
4720
|
-
].filter((p) => p.text !== "");
|
|
4721
|
-
}
|
|
4722
|
-
var view = (kind, parts) => ({ kind, text: parts.map((p) => p.text).join(""), parts });
|
|
4723
|
-
function devPromptPreview({ slug, ...i }) {
|
|
4724
|
-
const branch = devBranch(PLACEHOLDERS.issue, slug);
|
|
4725
|
-
return view("dev", promptParts((brief) => devSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch }), i.agent.brief));
|
|
4726
|
-
}
|
|
4727
|
-
function qaPromptPreview({ slug, ...i }) {
|
|
4728
|
-
const pr = { number: PLACEHOLDERS.pr, title: PLACEHOLDERS.prTitle, headRefName: PLACEHOLDERS.branch, url: PLACEHOLDERS.prUrl };
|
|
4729
|
-
const branch = qaBranch(PLACEHOLDERS.pr, slug);
|
|
4730
|
-
return view("qa", promptParts((brief) => qaSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch, pr }), i.agent.brief));
|
|
4824
|
+
// server/qaRetry.ts
|
|
4825
|
+
var QA_RESUME_PROMPT = "Your turn ended without your QA report. Finish what you were checking in the foreground, then end with the JSON report.";
|
|
4826
|
+
function qaRetry(e) {
|
|
4827
|
+
if (e.stopped) return "give-up";
|
|
4828
|
+
if (e.report) return "report";
|
|
4829
|
+
return e.ok && !e.limited && e.canResume ? "resume" : "give-up";
|
|
4731
4830
|
}
|
|
4732
|
-
var ceoPromptPreview = (text) => view("ceo", [{ label: OFFICE, text, editable: false }]);
|
|
4733
4831
|
|
|
4734
4832
|
// server/pacing.ts
|
|
4735
4833
|
var PACING_MS = 60 * 6e4;
|
|
@@ -4768,6 +4866,32 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
4768
4866
|
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.`;
|
|
4769
4867
|
}
|
|
4770
4868
|
|
|
4869
|
+
// server/deskTrim.ts
|
|
4870
|
+
var TRIM_SWEEP_MS = 15 * 6e4;
|
|
4871
|
+
var DEFAULT_TRIM_IDLE_MIN = 120;
|
|
4872
|
+
var MAX_TRIM_IDLE_MIN = 7 * 24 * 60;
|
|
4873
|
+
var idleSince = (endedAt, busyAt, bootAt) => Math.max(endedAt ?? bootAt, busyAt ?? 0);
|
|
4874
|
+
function desksToTrim(desks, thresholdMin, now2) {
|
|
4875
|
+
if (!(thresholdMin > 0)) return [];
|
|
4876
|
+
return desks.filter((d) => !d.busy && now2 - d.idleSince > thresholdMin * 6e4 && (d.trimmedAt === null || d.trimmedAt < d.idleSince)).map((d) => d.key);
|
|
4877
|
+
}
|
|
4878
|
+
function clampTrimIdleMin(v) {
|
|
4879
|
+
const n = Math.round(Number(v));
|
|
4880
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(MAX_TRIM_IDLE_MIN, n)) : DEFAULT_TRIM_IDLE_MIN;
|
|
4881
|
+
}
|
|
4882
|
+
function formatBytes(n) {
|
|
4883
|
+
const units = ["bytes", "KB", "MB", "GB", "TB"];
|
|
4884
|
+
let v = n;
|
|
4885
|
+
let i = 0;
|
|
4886
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
4887
|
+
v /= 1024;
|
|
4888
|
+
i++;
|
|
4889
|
+
}
|
|
4890
|
+
if (i === 0) return `${Math.round(n)} bytes`;
|
|
4891
|
+
return `${v < 9.95 ? v.toFixed(1) : Math.round(v)} ${units[i]}`;
|
|
4892
|
+
}
|
|
4893
|
+
var freedMessage = (bytes, desks) => `\u{1F9F9} Freed ${formatBytes(bytes)} from ${desks} idle desk${desks === 1 ? "" : "s"}`;
|
|
4894
|
+
|
|
4771
4895
|
// server/terminal.ts
|
|
4772
4896
|
import fs12 from "node:fs/promises";
|
|
4773
4897
|
import path14 from "node:path";
|
|
@@ -5146,6 +5270,7 @@ var Swarm = class {
|
|
|
5146
5270
|
tutorialStep: 0,
|
|
5147
5271
|
autoUpdate: true,
|
|
5148
5272
|
pacingSessions: DEFAULT_PACING_SESSIONS,
|
|
5273
|
+
trimIdleDesksMin: DEFAULT_TRIM_IDLE_MIN,
|
|
5149
5274
|
voice: { ...DEFAULT_VOICE }
|
|
5150
5275
|
},
|
|
5151
5276
|
repos: [],
|
|
@@ -5260,6 +5385,7 @@ var Swarm = class {
|
|
|
5260
5385
|
if (!EFFORTS.includes(this.state.settings.defaultEffort)) this.state.settings.defaultEffort = "medium";
|
|
5261
5386
|
if (this.state.settings.runtime !== "sdk") this.state.settings.runtime = "terminal";
|
|
5262
5387
|
if (!isCli(this.state.settings.defaultCli)) this.state.settings.defaultCli = "claude";
|
|
5388
|
+
this.state.settings.trimIdleDesksMin = clampTrimIdleMin(this.state.settings.trimIdleDesksMin);
|
|
5263
5389
|
if (!this.state.settings.defaultModel && this.state.settings.defaultCli === "claude") this.state.settings.defaultModel = DEFAULT_MODEL;
|
|
5264
5390
|
const old = this.state.settings;
|
|
5265
5391
|
if (old.maxConcurrent !== void 0) {
|
|
@@ -5340,6 +5466,7 @@ var Swarm = class {
|
|
|
5340
5466
|
setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
|
|
5341
5467
|
setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
|
|
5342
5468
|
setInterval(() => void this.saveTerminals(), TERMINAL_SAVE_MS);
|
|
5469
|
+
setInterval(() => void this.trimIdleDesks(), this.backend.demo ? 6e4 : TRIM_SWEEP_MS);
|
|
5343
5470
|
setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.sweepFloor(r.id), i * 1500)), DESK_SWEEP_INTERVAL_MS);
|
|
5344
5471
|
void this.backend.detectClis().then((clis) => {
|
|
5345
5472
|
this.clis = clis;
|
|
@@ -6612,13 +6739,24 @@ Note from the manager: ${note}` : ""
|
|
|
6612
6739
|
Linked issue: ${issue2.title}
|
|
6613
6740
|
${issue2.body.trim() || "(no description)"}` : ""
|
|
6614
6741
|
].filter((l) => l !== "").join("\n");
|
|
6615
|
-
|
|
6742
|
+
const systemAppend = this.buildQaSystemAppend(a, repo, cwd, branch, pr, qa.testStep, desk.deps);
|
|
6743
|
+
this.agentRt.get(a.id).qaResume = { cwd, systemAppend };
|
|
6744
|
+
this.startAgentSession(a, repo, cwd, prompt, systemAppend, void 0, QA_SCHEMA);
|
|
6616
6745
|
}
|
|
6617
6746
|
async onQaFinished(a, repo, result) {
|
|
6618
6747
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
6619
6748
|
const rt = this.agentRt.get(a.id);
|
|
6620
6749
|
const report = result.ok ? parseReport(result) : null;
|
|
6621
|
-
|
|
6750
|
+
const resume = rt.qaResume;
|
|
6751
|
+
const next = qaRetry({ stopped: a.status === "stopped", limited: this.limited(), ok: result.ok, report: !!report, canResume: !!resume && !!a.sessionId });
|
|
6752
|
+
if (next === "resume" && resume && a.sessionId) {
|
|
6753
|
+
rt.qaResume = null;
|
|
6754
|
+
a.endedAt = null;
|
|
6755
|
+
this.appendLog(a, [{ kind: "system", text: "\u21BB The session ended without a QA report. Resuming it once to finish." }]);
|
|
6756
|
+
this.startAgentSession(a, repo, resume.cwd, QA_RESUME_PROMPT, resume.systemAppend, a.sessionId, QA_SCHEMA);
|
|
6757
|
+
return;
|
|
6758
|
+
}
|
|
6759
|
+
if (next === "give-up" || !report) {
|
|
6622
6760
|
if (a.status !== "stopped") this.fail(a, { ...result, errors: result.errors.length ? result.errors : ["QA finished without a usable report"] }, `QA of PR #${a.prNumber}`);
|
|
6623
6761
|
if (rec) {
|
|
6624
6762
|
const failures = rec.sessionFailures + (this.limited() ? 0 : 1);
|
|
@@ -6637,17 +6775,17 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
6637
6775
|
let commentUrl = null;
|
|
6638
6776
|
if (rec) {
|
|
6639
6777
|
const pull = !pass ? await this.backend.prDetails(repo.fullName, rec.prNumber).catch(() => null) : null;
|
|
6640
|
-
const
|
|
6778
|
+
const next2 = qaOutcome(pass, rec, pull);
|
|
6641
6779
|
try {
|
|
6642
6780
|
this.appendLog(a, [{ kind: "system", text: "\u{1F4CE} Uploading evidence and posting the QA report on the PR\u2026" }]);
|
|
6643
|
-
const body2 = await this.renderQaComment(a, repo, rec, report, rt.shots,
|
|
6781
|
+
const body2 = await this.renderQaComment(a, repo, rec, report, rt.shots, next2);
|
|
6644
6782
|
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber, body2) || null;
|
|
6645
6783
|
this.appendLog(a, [{ kind: "system", text: ` \u23BF ${commentUrl ?? "comment posted"}` }]);
|
|
6646
6784
|
} catch (err) {
|
|
6647
6785
|
this.appendLog(a, [{ kind: "error", text: ` \u23BF Could not post the QA report: ${err.message}` }]);
|
|
6648
6786
|
}
|
|
6649
6787
|
const qaRounds = rec.round - rec.retests;
|
|
6650
|
-
const nextStatus =
|
|
6788
|
+
const nextStatus = next2 === "conflict" ? "failed" : next2;
|
|
6651
6789
|
const fixInstructions = report.fixInstructions ?? report.checks.filter((c) => c.result === "fail").map((c) => `${c.name}: ${c.details}`).join("\n");
|
|
6652
6790
|
this.setQa(rec, {
|
|
6653
6791
|
status: nextStatus,
|
|
@@ -6664,10 +6802,10 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
6664
6802
|
alerted: false,
|
|
6665
6803
|
qaChecks: pull?.checks ?? null
|
|
6666
6804
|
});
|
|
6667
|
-
if (
|
|
6805
|
+
if (next2 === "conflict") this.sendBack(repo, rec, "conflict", conflictFixInstructions(fixInstructions, repo.defaultBranch), false);
|
|
6668
6806
|
this.toast(
|
|
6669
6807
|
pass ? "success" : "error",
|
|
6670
|
-
pass ? `\u2705 ${a.name} passed PR #${rec.prNumber}${repo.autoMerge ? "; it merges once GitHub's checks are green" : ": ready to merge"}` : nextStatus === "needs-human" ? `\u274C PR #${rec.prNumber} failed QA ${qaRounds} times and needs a human` :
|
|
6808
|
+
pass ? `\u2705 ${a.name} passed PR #${rec.prNumber}${repo.autoMerge ? "; it merges once GitHub's checks are green" : ": ready to merge"}` : nextStatus === "needs-human" ? `\u274C PR #${rec.prNumber} failed QA ${qaRounds} times and needs a human` : next2 === "conflict" ? `\u274C ${a.name} failed PR #${rec.prNumber}, and it conflicts with ${repo.defaultBranch}; sending it back to merge and fix` : `\u274C ${a.name} failed PR #${rec.prNumber}; sending it back to the developer`
|
|
6671
6809
|
);
|
|
6672
6810
|
}
|
|
6673
6811
|
}
|
|
@@ -6864,6 +7002,10 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6864
7002
|
if (patch.tutorialStep !== void 0) s2.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
6865
7003
|
if (typeof patch.autoUpdate === "boolean") s2.autoUpdate = patch.autoUpdate;
|
|
6866
7004
|
if (patch.pacingSessions !== void 0) s2.pacingSessions = clampPacingSessions(patch.pacingSessions);
|
|
7005
|
+
if (patch.trimIdleDesksMin !== void 0) {
|
|
7006
|
+
s2.trimIdleDesksMin = clampTrimIdleMin(patch.trimIdleDesksMin);
|
|
7007
|
+
setTimeout(() => void this.trimIdleDesks(), 1e3);
|
|
7008
|
+
}
|
|
6867
7009
|
if (patch.voice !== void 0) s2.voice = voiceSettings(s2.voice, patch.voice);
|
|
6868
7010
|
this.save();
|
|
6869
7011
|
this.broadcast({ type: "settings", settings: s2 });
|
|
@@ -7440,6 +7582,63 @@ ${t}`);
|
|
|
7440
7582
|
}
|
|
7441
7583
|
this.enqueueCeo({ kind: "review", at: Date.now() });
|
|
7442
7584
|
}
|
|
7585
|
+
// ---------- idle desks ----------
|
|
7586
|
+
bootAt = Date.now();
|
|
7587
|
+
deskWatch = /* @__PURE__ */ new Map();
|
|
7588
|
+
// desk → what the sweeps saw
|
|
7589
|
+
trimming = false;
|
|
7590
|
+
/** An agent's desk is in use: a task (preparing, working, testing or fixing) or a live CLI in their terminal. Hands off a fired agent's. */
|
|
7591
|
+
deskBusy(a) {
|
|
7592
|
+
const rt = this.agentRt.get(a.id);
|
|
7593
|
+
return !rt || !this.state.agents.includes(a) || BUSY.includes(a.status) || !!rt.session || !!rt.terminal?.live;
|
|
7594
|
+
}
|
|
7595
|
+
/**
|
|
7596
|
+
* Free disk space on desks idle longer than settings.trimIdleDesksMin: every agent's desk, and each floor's preview
|
|
7597
|
+
* worktree while no preview runs. One desk at a time, once per idle stretch; one phone message per sweep that freed anything.
|
|
7598
|
+
*/
|
|
7599
|
+
async trimIdleDesks() {
|
|
7600
|
+
const min = this.state.settings.trimIdleDesksMin;
|
|
7601
|
+
if (!min || this.trimming) return;
|
|
7602
|
+
this.trimming = true;
|
|
7603
|
+
try {
|
|
7604
|
+
const now2 = Date.now();
|
|
7605
|
+
const desks = [];
|
|
7606
|
+
for (const a of this.state.agents) {
|
|
7607
|
+
const repo = this.state.repos.find((r) => r.id === a.repoId);
|
|
7608
|
+
if (repo) desks.push({ key: a.id, repo, slug: this.agentSlug(a), endedAt: a.endedAt, busy: () => this.deskBusy(a) });
|
|
7609
|
+
}
|
|
7610
|
+
for (const repo of this.state.repos) {
|
|
7611
|
+
desks.push({ key: `preview:${repo.id}`, repo, slug: PREVIEW_SLUG, endedAt: null, busy: () => !this.state.repos.includes(repo) || this.previews.active(repo) });
|
|
7612
|
+
}
|
|
7613
|
+
for (const key of this.deskWatch.keys()) if (!desks.some((d) => d.key === key)) this.deskWatch.delete(key);
|
|
7614
|
+
const seen = desks.map((d) => {
|
|
7615
|
+
const w = this.deskWatch.get(d.key) ?? { busyAt: null, trimmedAt: null };
|
|
7616
|
+
this.deskWatch.set(d.key, w);
|
|
7617
|
+
const busy = d.busy();
|
|
7618
|
+
if (busy) w.busyAt = now2;
|
|
7619
|
+
return { key: d.key, busy, idleSince: idleSince(d.endedAt, w.busyAt, this.bootAt), trimmedAt: w.trimmedAt };
|
|
7620
|
+
});
|
|
7621
|
+
const due = new Set(desksToTrim(seen, min, now2));
|
|
7622
|
+
let freed = 0;
|
|
7623
|
+
let count = 0;
|
|
7624
|
+
for (const d of desks.filter((x) => due.has(x.key))) {
|
|
7625
|
+
const result = await this.backend.trimDesk(d.repo.fullName, d.slug, () => !d.busy()).catch((err) => {
|
|
7626
|
+
console.warn(`could not trim desk ${d.slug} of ${d.repo.fullName}:`, oneLine(err));
|
|
7627
|
+
return void 0;
|
|
7628
|
+
});
|
|
7629
|
+
if (result === void 0) continue;
|
|
7630
|
+
if (!result?.skipped.length) this.deskWatch.get(d.key).trimmedAt = Date.now();
|
|
7631
|
+
if (!result?.freed) continue;
|
|
7632
|
+
freed += result.freed;
|
|
7633
|
+
count++;
|
|
7634
|
+
const locked = result.skipped.length ? `; still locked: ${result.skipped.join(", ")}` : "";
|
|
7635
|
+
console.log(`trimmed idle desk ${d.slug} of ${d.repo.fullName}: freed ${formatBytes(result.freed)} (${result.removed.join(", ")})${locked}`);
|
|
7636
|
+
}
|
|
7637
|
+
if (count) this.postMessage("office", freedMessage(freed, count));
|
|
7638
|
+
} finally {
|
|
7639
|
+
this.trimming = false;
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7443
7642
|
// ---------- the phone ----------
|
|
7444
7643
|
postMessage(from, text, requestId) {
|
|
7445
7644
|
const m = { id: this.messageSeq++, from, text: text.trim().slice(0, 6e3), at: Date.now(), ...requestId ? { requestId } : {} };
|