cubefarm 0.3.12 → 0.3.13
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-CAwKknk0.js → ToyWorld-aw61IQYN.js} +2 -2
- package/dist/assets/{index-BCeYUClc.js → index-BljMMlNA.js} +58 -57
- package/dist/assets/{rapier-BRTeZJBn.js → rapier-3MlT3vHU.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/index.js +702 -678
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
package/dist-server/index.js
CHANGED
|
@@ -725,8 +725,8 @@ Write-Output $kill.Count`;
|
|
|
725
725
|
}
|
|
726
726
|
|
|
727
727
|
// server/agentRunner.ts
|
|
728
|
-
import
|
|
729
|
-
import
|
|
728
|
+
import fs5 from "node:fs";
|
|
729
|
+
import path5 from "node:path";
|
|
730
730
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
731
731
|
|
|
732
732
|
// server/browser.ts
|
|
@@ -764,499 +764,127 @@ function browserProblem(mcp, chrome = hasChrome()) {
|
|
|
764
764
|
return null;
|
|
765
765
|
}
|
|
766
766
|
|
|
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
|
-
|
|
767
|
+
// server/clis.ts
|
|
768
|
+
import fs4 from "node:fs";
|
|
769
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
770
|
+
import path4 from "node:path";
|
|
771
|
+
var CLIS = [
|
|
772
|
+
{ id: "claude", label: "Claude Code", command: "claude", integrated: true },
|
|
773
|
+
{ id: "codex", label: "Codex", command: "codex", integrated: false },
|
|
774
|
+
{ id: "opencode", label: "OpenCode", command: "opencode", integrated: false }
|
|
775
|
+
];
|
|
776
|
+
var isCli = (v) => CLIS.some((c) => c.id === v);
|
|
777
|
+
var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
|
|
778
|
+
var WIN = process.platform === "win32";
|
|
779
|
+
function resolveCommand(name, env = process.env) {
|
|
780
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
781
|
+
const dirs = (env[key] ?? "").split(path4.delimiter).filter(Boolean);
|
|
782
|
+
const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
|
|
783
|
+
for (const dir of dirs) {
|
|
784
|
+
for (const ext of exts) {
|
|
785
|
+
const file = path4.join(dir, name + ext.toLowerCase());
|
|
786
|
+
try {
|
|
787
|
+
const st = fs4.statSync(file);
|
|
788
|
+
if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
|
|
789
|
+
} catch {
|
|
790
790
|
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
var allowAll = async (_toolName, input) => ({ behavior: "allow", updatedInput: input });
|
|
795
|
-
function screenshotFile(cwd, text) {
|
|
796
|
-
const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
|
|
797
|
-
if (!file) return null;
|
|
798
|
-
try {
|
|
799
|
-
return { data: fs4.readFileSync(path4.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
|
|
800
|
-
} catch {
|
|
801
|
-
return null;
|
|
791
|
+
}
|
|
802
792
|
}
|
|
793
|
+
return null;
|
|
803
794
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
795
|
+
function unwrapCmdShim(shimPath, text) {
|
|
796
|
+
const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
|
|
797
|
+
if (!line) return null;
|
|
798
|
+
const dir = path4.dirname(shimPath);
|
|
799
|
+
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path4.join(dir, m[1]));
|
|
800
|
+
const target = targets[targets.length - 1];
|
|
801
|
+
if (!target) return null;
|
|
802
|
+
if (/\.exe$/i.test(target)) return { file: target, args: [] };
|
|
803
|
+
if (!/\.[cm]?js$/i.test(target)) return null;
|
|
804
|
+
const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
|
|
805
|
+
const nodeFlags = line.includes('"%_prog%"') ? between.trim().split(/\s+/).filter((f) => f.startsWith("--")) : [];
|
|
806
|
+
const localNode = path4.join(dir, "node.exe");
|
|
807
|
+
return { file: fs4.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
|
|
808
|
+
}
|
|
809
|
+
var bundled;
|
|
810
|
+
function bundledClaude() {
|
|
811
|
+
if (bundled !== void 0) return bundled;
|
|
812
|
+
bundled = null;
|
|
813
|
+
let sdk;
|
|
807
814
|
try {
|
|
808
|
-
|
|
815
|
+
sdk = createRequire2(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
809
816
|
} catch {
|
|
810
|
-
return
|
|
817
|
+
return null;
|
|
811
818
|
}
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
819
|
+
const { platform, arch } = process;
|
|
820
|
+
const report = platform === "linux" ? process.report?.getReport() : void 0;
|
|
821
|
+
const musl = platform === "linux" && !report?.header?.glibcVersionRuntime;
|
|
822
|
+
const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
823
|
+
for (const target of targets) {
|
|
817
824
|
try {
|
|
818
|
-
|
|
825
|
+
bundled = createRequire2(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
|
|
826
|
+
return bundled;
|
|
819
827
|
} catch {
|
|
820
|
-
continue;
|
|
821
828
|
}
|
|
822
|
-
found2.push({ name, file, mime });
|
|
823
|
-
}
|
|
824
|
-
return found2;
|
|
825
|
-
}
|
|
826
|
-
var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
|
|
827
|
-
function rel(cwd, p) {
|
|
828
|
-
const s2 = String(p ?? "");
|
|
829
|
-
if (!s2) return "";
|
|
830
|
-
const r = path4.relative(cwd, path4.resolve(cwd, s2));
|
|
831
|
-
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s2;
|
|
832
|
-
}
|
|
833
|
-
var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
834
|
-
function tidyPaths(cwd, text) {
|
|
835
|
-
let out = text;
|
|
836
|
-
for (const v of /* @__PURE__ */ new Set([cwd, cwd.replaceAll("\\", "/")])) {
|
|
837
|
-
out = out.replace(new RegExp(`^cd\\s+"?${escapeRe(v)}"?\\s*(&&|;)\\s*`, "i"), "");
|
|
838
|
-
out = out.replace(new RegExp(`${escapeRe(v)}[\\\\/]?`, "gi"), "./");
|
|
839
829
|
}
|
|
840
|
-
return
|
|
830
|
+
return null;
|
|
841
831
|
}
|
|
842
|
-
function
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
return
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
return `Edit ${rel(cwd, input.file_path)}`;
|
|
854
|
-
case "Glob":
|
|
855
|
-
return `Glob ${input.pattern}`;
|
|
856
|
-
case "Grep":
|
|
857
|
-
return `Grep "${clip(String(input.pattern ?? ""), 80)}"`;
|
|
858
|
-
case "WebFetch":
|
|
859
|
-
return `Fetch ${input.url}`;
|
|
860
|
-
case "WebSearch":
|
|
861
|
-
return `Search "${input.query}"`;
|
|
862
|
-
case "Task":
|
|
863
|
-
case "Agent":
|
|
864
|
-
return `Subagent: ${input.description ?? ""}`;
|
|
865
|
-
case "TodoWrite":
|
|
866
|
-
return "Update todo list";
|
|
867
|
-
case "ToolSearch":
|
|
868
|
-
return `Load tools ${String(input.query ?? "").replace(/^select:/, "")}`;
|
|
869
|
-
case "apply_patch": {
|
|
870
|
-
const files = [...String(input.command ?? "").matchAll(/^\*\*\* (Add|Update|Delete) File: (.+)$/gm)].map((m) => `${m[1] === "Update" ? "" : `${m[1].toLowerCase()} `}${rel(cwd, m[2].trim())}`);
|
|
871
|
-
return `Edit ${clip(files.join(", ") || "files", 200)}`;
|
|
832
|
+
function commandFor(id) {
|
|
833
|
+
const bundled2 = id === "claude" ? bundledClaude() : null;
|
|
834
|
+
if (bundled2) return { file: bundled2, args: [] };
|
|
835
|
+
const def = CLIS.find((c) => c.id === id);
|
|
836
|
+
const found2 = def && resolveCommand(def.command);
|
|
837
|
+
if (!found2) return null;
|
|
838
|
+
if (WIN && /\.(cmd|bat)$/i.test(found2)) {
|
|
839
|
+
try {
|
|
840
|
+
return unwrapCmdShim(found2, fs4.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
|
|
841
|
+
} catch {
|
|
842
|
+
return { file: found2, args: [] };
|
|
872
843
|
}
|
|
873
844
|
}
|
|
874
|
-
|
|
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
|
-
}
|
|
882
|
-
function describeOfficeTool(action, input) {
|
|
883
|
-
const floor = input.floor != null ? ` \u2192 floor ${input.floor}` : "";
|
|
884
|
-
switch (action) {
|
|
885
|
-
case "company_status":
|
|
886
|
-
return "\u{1F3E2} company_status";
|
|
887
|
-
case "agent_detail":
|
|
888
|
-
return `\u{1F50E} agent_detail ${String(input.agent_id ?? "")}`;
|
|
889
|
-
case "set_floor_profile":
|
|
890
|
-
return `\u{1F5C2}\uFE0F set_floor_profile${floor}${input.summary ? `: ${clip(String(input.summary), 90)}` : ""}`;
|
|
891
|
-
case "update_job":
|
|
892
|
-
return `\u{1FAAA} update_job ${input.title ? `"${clip(String(input.title), 60)}"` : String(input.agent_id ?? "")}`;
|
|
893
|
-
case "propose_hire":
|
|
894
|
-
return `\u{1F91D} propose_hire ${input.role === "qa" ? "QA " : ""}"${clip(String(input.title ?? ""), 60)}"${floor}`;
|
|
895
|
-
case "propose_let_go":
|
|
896
|
-
return `\u{1F44B} propose_let_go ${String(input.agent_id ?? "")}`;
|
|
897
|
-
case "file_issue":
|
|
898
|
-
return `\u{1F4DD} file_issue "${clip(String(input.title ?? ""), 70)}"${floor}${input.specialty ? ` \xB7 ${input.specialty}` : ""}`;
|
|
899
|
-
case "close_issue":
|
|
900
|
-
return `\u{1F5C2}\uFE0F close_issue #${String(input.number ?? "?")}${floor}`;
|
|
901
|
-
case "route_issue":
|
|
902
|
-
return `\u{1F500} route_issue #${String(input.number ?? "?")}${floor}${input.specialty !== void 0 ? ` \xB7 ${input.specialty || "no specialty"}` : ""}${Array.isArray(input.depends_on) ? ` \xB7 depends on ${input.depends_on.map((n) => `#${n}`).join(", ") || "nothing"}` : ""}`;
|
|
903
|
-
}
|
|
904
|
-
return `\u{1F3E2} ${action}(${clip(JSON.stringify(input), 120)})`;
|
|
905
|
-
}
|
|
906
|
-
function todoLines(input) {
|
|
907
|
-
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
908
|
-
return todos.slice(0, 12).map((t) => ({
|
|
909
|
-
kind: "result",
|
|
910
|
-
text: ` ${t.status === "completed" ? "\u2612" : t.status === "in_progress" ? "\u25D0" : "\u2610"} ${clip(t.content ?? "", 150)}`
|
|
911
|
-
}));
|
|
845
|
+
return { file: found2, args: [] };
|
|
912
846
|
}
|
|
913
|
-
function
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
847
|
+
async function detectClis() {
|
|
848
|
+
return Promise.all(
|
|
849
|
+
CLIS.map(async (c) => {
|
|
850
|
+
const cmd = commandFor(c.id);
|
|
851
|
+
const version = cmd ? await run(cmd.file, [...cmd.args, "--version"], { timeoutMs: 2e4 }).catch(() => null) : null;
|
|
852
|
+
return { id: c.id, label: c.label, installed: !!cmd, version: version?.split(/\r?\n/)[0].trim().slice(0, 60) || null, integrated: c.integrated };
|
|
853
|
+
})
|
|
854
|
+
);
|
|
919
855
|
}
|
|
920
|
-
var
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if (tool2 === "Write") return [{ kind: "result", text: " \u23BF Saved" }];
|
|
925
|
-
if (tool2 === "Read") return [{ kind: "result", text: ` \u23BF Read ${lineCount(text)} lines` }];
|
|
926
|
-
if (tool2 === "Glob") return [{ kind: "result", text: ` \u23BF Found ${lineCount(text)} files` }];
|
|
927
|
-
const lines = tidyPaths(cwd, text).replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "");
|
|
928
|
-
if (lines.length === 0) return [{ kind: "result", text: " \u23BF (no output)" }];
|
|
929
|
-
const shown = lines.slice(0, 3).map((l, i) => ({ kind: "result", text: `${i === 0 ? " \u23BF " : " "}${clip(l, 170)}` }));
|
|
930
|
-
if (lines.length > 3) shown.push({ kind: "result", text: ` \u2026 +${lines.length - 3} lines` });
|
|
931
|
-
return shown;
|
|
856
|
+
var EFFORT_CODEX = { low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "xhigh" };
|
|
857
|
+
var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Interrupt"];
|
|
858
|
+
function codexHookCommand(node, script, win = WIN) {
|
|
859
|
+
return win ? `node "${script}"` : [node, script].map((s2) => `'${s2.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
932
860
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
if (browser) mcpServers.playwright = browser;
|
|
961
|
-
const problem = browserProblem(browser);
|
|
962
|
-
if (problem) cb.log([{ kind: "error", text: problem }]);
|
|
963
|
-
}
|
|
964
|
-
if (opts.office) mcpServers.office = opts.office.server;
|
|
965
|
-
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"];
|
|
966
|
-
if (opts.role === "ceo") disallowedTools.push("Bash", "PowerShell", "NotebookEdit");
|
|
967
|
-
const options = {
|
|
968
|
-
cwd: opts.cwd,
|
|
969
|
-
additionalDirectories: opts.additionalDirectories,
|
|
970
|
-
abortController: abort,
|
|
971
|
-
env,
|
|
972
|
-
model: opts.model || void 0,
|
|
973
|
-
effort: opts.effort,
|
|
974
|
-
// The manager's own setup (settings, skills, plugins, MCP servers, connectors), plus the office's servers below.
|
|
975
|
-
settingSources: ["user", "project", "local"],
|
|
976
|
-
systemPrompt: { type: "preset", preset: "claude_code", append: opts.systemAppend },
|
|
977
|
-
mcpServers,
|
|
978
|
-
disallowedTools,
|
|
979
|
-
resume: opts.resumeSessionId,
|
|
980
|
-
stderr: (data) => {
|
|
981
|
-
const line = data.trim();
|
|
982
|
-
if (line && /error|fail/i.test(line)) cb.log([{ kind: "error", text: clip(line, 200) }]);
|
|
983
|
-
},
|
|
984
|
-
permissionMode: "acceptEdits",
|
|
985
|
-
canUseTool: allowAll,
|
|
986
|
-
...opts.outputSchema ? { outputFormat: { type: "json_schema", schema: opts.outputSchema } } : {}
|
|
987
|
-
};
|
|
988
|
-
let emptyTurn;
|
|
989
|
-
const answered = (msg) => {
|
|
990
|
-
pendingTurns -= 1;
|
|
991
|
-
if (pendingTurns > 0) return;
|
|
992
|
-
input.close();
|
|
993
|
-
const ok = msg.subtype === "success" && !msg.is_error;
|
|
994
|
-
cb.finished({
|
|
995
|
-
ok,
|
|
996
|
-
text: msg.subtype === "success" ? msg.result : "",
|
|
997
|
-
costUsd: lastCost,
|
|
998
|
-
turns: lastTurns,
|
|
999
|
-
errors: msg.subtype === "success" ? [] : msg.errors,
|
|
1000
|
-
structured: msg.subtype === "success" ? msg.structured_output : void 0
|
|
1001
|
-
});
|
|
1002
|
-
};
|
|
1003
|
-
const handle = (msg) => {
|
|
1004
|
-
clearTimeout(emptyTurn);
|
|
1005
|
-
if (done && msg.type === "assistant" && !stopped) {
|
|
1006
|
-
stopped = true;
|
|
1007
|
-
cb.log([{ kind: "system", text: "\u25A0 This session kept working after it had finished, so it was stopped." }]);
|
|
1008
|
-
abort.abort();
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
switch (msg.type) {
|
|
1012
|
-
case "system": {
|
|
1013
|
-
if (msg.subtype === "init") {
|
|
1014
|
-
cb.sessionId(msg.session_id);
|
|
1015
|
-
const mcp = msg.mcp_servers.map((s2) => `${s2.name}:${s2.status}`).join(", ");
|
|
1016
|
-
cb.log([
|
|
1017
|
-
{ kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort` },
|
|
1018
|
-
{ kind: "system", text: ` cwd ${msg.cwd}${mcp ? ` \xB7 mcp ${mcp}` : ""}` }
|
|
1019
|
-
]);
|
|
1020
|
-
} else if (msg.subtype === "api_retry") {
|
|
1021
|
-
cb.log([{ kind: "system", text: `\u2026 API retry ${msg.attempt}/${msg.max_retries} in ${Math.round(msg.retry_delay_ms / 1e3)}s` }]);
|
|
1022
|
-
} else if (msg.subtype === "compact_boundary") {
|
|
1023
|
-
cb.log([{ kind: "system", text: "\u273B Conversation compacted" }]);
|
|
1024
|
-
}
|
|
1025
|
-
break;
|
|
1026
|
-
}
|
|
1027
|
-
case "rate_limit_event": {
|
|
1028
|
-
const info = msg.rate_limit_info;
|
|
1029
|
-
if (info.status !== "allowed") {
|
|
1030
|
-
const when = info.resetsAt ? new Date(info.resetsAt * 1e3).toLocaleTimeString() : "later";
|
|
1031
|
-
cb.log([{ kind: "error", text: `\u26A0 Subscription usage ${info.status === "rejected" ? "limit reached" : "warning"} (${info.rateLimitType ?? "limit"}) \xB7 resets ${when}` }]);
|
|
1032
|
-
const overage = info.isUsingOverage || info.overageStatus === "allowed" || info.overageStatus === "allowed_warning";
|
|
1033
|
-
if (info.status === "rejected" && !overage) cb.limited?.(info.resetsAt ? info.resetsAt * 1e3 : null);
|
|
1034
|
-
if (info.status === "allowed_warning" && !overage) {
|
|
1035
|
-
cb.usageWarning?.({ resetsAt: info.resetsAt ? info.resetsAt * 1e3 : null, rateLimitType: info.rateLimitType ?? null, utilization: info.utilization ?? null });
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
break;
|
|
1039
|
-
}
|
|
1040
|
-
case "assistant": {
|
|
1041
|
-
if (msg.parent_tool_use_id) break;
|
|
1042
|
-
const out = [];
|
|
1043
|
-
for (const block of msg.message.content) {
|
|
1044
|
-
if (block.type === "text" && block.text.trim()) {
|
|
1045
|
-
const lines = block.text.trim().split("\n");
|
|
1046
|
-
lines.slice(0, 30).forEach((l, i) => out.push({ kind: "text", text: `${i === 0 ? "\u25CF " : " "}${l}` }));
|
|
1047
|
-
if (lines.length > 30) out.push({ kind: "text", text: ` \u2026 (${lines.length - 30} more lines)` });
|
|
1048
|
-
} else if (block.type === "thinking") {
|
|
1049
|
-
out.push({ kind: "thinking", text: "\u273B Thinking\u2026" });
|
|
1050
|
-
} else if (block.type === "tool_use") {
|
|
1051
|
-
const toolInput2 = block.input ?? {};
|
|
1052
|
-
toolNames.set(block.id, block.name);
|
|
1053
|
-
out.push({ kind: "tool", tool: block.name, text: `\u23FA ${describeTool(opts.cwd, block.name, toolInput2)}` });
|
|
1054
|
-
if (block.name === "TodoWrite") out.push(...todoLines(toolInput2));
|
|
1055
|
-
cb.tool(block.name);
|
|
1056
|
-
if (block.name === "mcp__playwright__browser_navigate" && typeof toolInput2.url === "string") cb.browserUrl(toolInput2.url);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
if (out.length) cb.log(out);
|
|
1060
|
-
break;
|
|
1061
|
-
}
|
|
1062
|
-
case "user": {
|
|
1063
|
-
if (msg.parent_tool_use_id) break;
|
|
1064
|
-
const content = msg.message.content;
|
|
1065
|
-
if (!Array.isArray(content)) break;
|
|
1066
|
-
const out = [];
|
|
1067
|
-
for (const block of content) {
|
|
1068
|
-
if (block.type !== "tool_result") continue;
|
|
1069
|
-
const name = toolNames.get(block.tool_use_id) ?? "";
|
|
1070
|
-
const text = resultText(block.content);
|
|
1071
|
-
let images = 0;
|
|
1072
|
-
if (Array.isArray(block.content)) {
|
|
1073
|
-
for (const part of block.content) {
|
|
1074
|
-
if (part.type === "image" && part.source.type === "base64") {
|
|
1075
|
-
images += 1;
|
|
1076
|
-
cb.screenshot(Buffer.from(part.source.data, "base64"), part.source.media_type);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
const saved = name === "mcp__playwright__browser_take_screenshot" && !images ? screenshotFile(opts.cwd, text) : null;
|
|
1081
|
-
if (saved) cb.screenshot(saved.data, saved.mime);
|
|
1082
|
-
if (name.startsWith("mcp__playwright__")) {
|
|
1083
|
-
const url = text.match(/Page URL:\s*(\S+)/);
|
|
1084
|
-
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" } };
|
|
861
|
+
var CROSS_TURN_TOOLS = ["Monitor", "ScheduleWakeup", "CronCreate", "CronDelete", "CronList", "RemoteTrigger"];
|
|
862
|
+
function launchArgs(id, ctx) {
|
|
863
|
+
switch (id) {
|
|
864
|
+
case "claude": {
|
|
865
|
+
const disallowed = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", ...CROSS_TURN_TOOLS, ...ctx.role === "ceo" ? ["Bash", "PowerShell", "NotebookEdit"] : []];
|
|
866
|
+
const args = [
|
|
867
|
+
...ctx.resumeId ? ["--resume", ctx.resumeId] : ["--session-id", ctx.sessionId],
|
|
868
|
+
...ctx.model ? ["--model", ctx.model] : [],
|
|
869
|
+
...ctx.effort ? ["--effort", ctx.effort] : [],
|
|
870
|
+
// The office's PreToolUse hook answers every permission question (allow, or deny with an office rule).
|
|
871
|
+
"--permission-mode",
|
|
872
|
+
"acceptEdits",
|
|
873
|
+
// On top of the manager's own setup (settings, skills, plugins, MCP servers): the office's hooks and servers.
|
|
874
|
+
"--settings",
|
|
875
|
+
ctx.files.settings,
|
|
876
|
+
...ctx.files.mcp ? ["--mcp-config", ctx.files.mcp] : [],
|
|
877
|
+
"--append-system-prompt-file",
|
|
878
|
+
ctx.files.system,
|
|
879
|
+
"--name",
|
|
880
|
+
ctx.name,
|
|
881
|
+
...ctx.additionalDirectories.flatMap((d) => ["--add-dir", d]),
|
|
882
|
+
"--disallowedTools",
|
|
883
|
+
...disallowed,
|
|
884
|
+
"--",
|
|
885
|
+
ctx.prompt
|
|
886
|
+
];
|
|
887
|
+
return { args, env: { DISABLE_AUTOUPDATER: "1" } };
|
|
1260
888
|
}
|
|
1261
889
|
case "codex": {
|
|
1262
890
|
const toml = (s2) => JSON.stringify(s2);
|
|
@@ -1297,118 +925,491 @@ function launchArgs(id, ctx) {
|
|
|
1297
925
|
}
|
|
1298
926
|
}
|
|
1299
927
|
}
|
|
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;
|
|
928
|
+
function oneAtATime() {
|
|
929
|
+
const tails = /* @__PURE__ */ new Map();
|
|
930
|
+
return (key, step) => {
|
|
931
|
+
const next = (tails.get(key) ?? Promise.resolve()).then(step).then(
|
|
932
|
+
() => void 0,
|
|
933
|
+
() => void 0
|
|
934
|
+
);
|
|
935
|
+
tails.set(key, next);
|
|
936
|
+
void next.then(() => tails.get(key) === next && tails.delete(key));
|
|
937
|
+
return next;
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
var threadSteps = oneAtATime();
|
|
941
|
+
function codexThread(action, id, after) {
|
|
942
|
+
return threadSteps(id, async () => {
|
|
943
|
+
await after;
|
|
944
|
+
const cmd = commandFor("codex");
|
|
945
|
+
if (cmd) await run(cmd.file, [...cmd.args, action, id], { timeoutMs: 3e4 });
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
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;
|
|
949
|
+
function trustKey(screen) {
|
|
950
|
+
if (!TRUST_PROMPT.test(screen)) return null;
|
|
951
|
+
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
952
|
+
if (/^(yes|trust|continue|proceed)\b/i.test(selected.trim())) return "enter";
|
|
953
|
+
if (/^(no|exit|quit|cancel)\b/i.test(selected.trim())) return "down";
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
function hookReviewKey(screen) {
|
|
957
|
+
if (!/Hooks need review/i.test(screen)) return null;
|
|
958
|
+
const selected = screen.match(/^\s*[❯›]\s*(?:\d+\.\s*)?(.+)$/m)?.[1] ?? "";
|
|
959
|
+
return /^continue without trusting/i.test(selected.trim()) ? "enter" : "down";
|
|
960
|
+
}
|
|
961
|
+
function interruptions(screen) {
|
|
962
|
+
return screen.split("\n").filter((l) => /\binterrupted\b/i.test(l)).length;
|
|
963
|
+
}
|
|
964
|
+
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.
|
|
965
|
+
const url = process.argv[2];
|
|
966
|
+
let body = '';
|
|
967
|
+
process.stdin.on('data', (c) => (body += c));
|
|
968
|
+
process.stdin.on('end', async () => {
|
|
969
|
+
try {
|
|
970
|
+
const status = JSON.parse(body);
|
|
971
|
+
const res = await fetch(url, {
|
|
972
|
+
method: 'POST',
|
|
973
|
+
headers: { 'content-type': 'application/json' },
|
|
974
|
+
body: JSON.stringify({ hook_event_name: 'StatusLine', session_id: status.session_id, cost: status.cost, rate_limits: status.rate_limits }),
|
|
975
|
+
signal: AbortSignal.timeout(2000),
|
|
976
|
+
});
|
|
977
|
+
const out = await res.json();
|
|
978
|
+
process.stdout.write(String(out.statusLine ?? ''));
|
|
979
|
+
} catch {
|
|
980
|
+
process.stdout.write('cubefarm');
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
`;
|
|
984
|
+
var NOTIFY_SOURCE = String.raw`// cubefarm: Codex's notify program. Tells the office a turn is complete.
|
|
985
|
+
const [url, payload] = process.argv.slice(2);
|
|
986
|
+
let event = {};
|
|
987
|
+
try {
|
|
988
|
+
event = JSON.parse(payload ?? '{}');
|
|
989
|
+
} catch {}
|
|
990
|
+
if (event.type === 'agent-turn-complete') {
|
|
991
|
+
const input = String((event['input-messages'] ?? [])[0] ?? '').slice(0, 300);
|
|
992
|
+
fetch(url, {
|
|
993
|
+
method: 'POST',
|
|
994
|
+
headers: { 'content-type': 'application/json' },
|
|
995
|
+
body: JSON.stringify({ hook_event_name: 'TurnComplete', session_id: event['thread-id'] ?? null, last_assistant_message: event['last-assistant-message'] ?? '', input }),
|
|
996
|
+
signal: AbortSignal.timeout(3000),
|
|
997
|
+
}).catch(() => {});
|
|
998
|
+
}
|
|
999
|
+
`;
|
|
1000
|
+
var CODEX_HOOK_SOURCE = String.raw`// cubefarm: Codex's hooks. Tells the office what Codex is doing; never changes what it does.
|
|
1001
|
+
const url = process.env.CUBEFARM_HOOK_URL;
|
|
1002
|
+
const chunks = [];
|
|
1003
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
1004
|
+
process.stdin.on('end', async () => {
|
|
1005
|
+
try {
|
|
1006
|
+
if (url) await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: Buffer.concat(chunks), signal: AbortSignal.timeout(5000) });
|
|
1007
|
+
} catch {}
|
|
1008
|
+
process.stdout.write('{}');
|
|
1009
|
+
});
|
|
1010
|
+
`;
|
|
1011
|
+
var OPENCODE_PLUGIN_SOURCE = String.raw`// cubefarm: tells the office when OpenCode's session goes idle.
|
|
1012
|
+
export default async function CubefarmPlugin({ client } = {}) {
|
|
1013
|
+
const url = process.env.CUBEFARM_NOTIFY_URL;
|
|
1014
|
+
if (!url) return {};
|
|
1015
|
+
let root;
|
|
1016
|
+
const post = (body) =>
|
|
1017
|
+
fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(3000) }).catch(() => {});
|
|
1018
|
+
const lastReply = async (id) => {
|
|
1019
|
+
try {
|
|
1020
|
+
const res = await client.session.messages({ path: { id } });
|
|
1021
|
+
const reply = (res?.data ?? []).filter((m) => m?.info?.role === 'assistant').pop();
|
|
1022
|
+
return (reply?.parts ?? []).filter((p) => p?.type === 'text').map((p) => p.text).join('\n').trim();
|
|
1023
|
+
} catch {
|
|
1024
|
+
return '';
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
return {
|
|
1028
|
+
event: async ({ event }) => {
|
|
1029
|
+
const props = event?.properties ?? {};
|
|
1030
|
+
if (event?.type === 'session.created' && !props.info?.parentID && !root) root = props.info?.id;
|
|
1031
|
+
const sessionID = props.sessionID ?? props.info?.id;
|
|
1032
|
+
if (event?.type === 'session.idle' && (!root || sessionID === root)) {
|
|
1033
|
+
post({ hook_event_name: 'TurnComplete', session_id: sessionID ?? null, last_assistant_message: sessionID ? await lastReply(sessionID) : '' });
|
|
1034
|
+
}
|
|
1035
|
+
if (event?.type === 'session.error') post({ hook_event_name: 'TurnError', session_id: sessionID ?? null, error: String(props.error?.data?.message ?? props.error?.name ?? 'error') });
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
`;
|
|
1040
|
+
|
|
1041
|
+
// server/agentRunner.ts
|
|
1042
|
+
var InputQueue = class {
|
|
1043
|
+
items = [];
|
|
1044
|
+
waiters = [];
|
|
1045
|
+
closed = false;
|
|
1046
|
+
push(text) {
|
|
1047
|
+
if (this.closed) return;
|
|
1048
|
+
const msg = { type: "user", message: { role: "user", content: text }, parent_tool_use_id: null };
|
|
1049
|
+
const waiter = this.waiters.shift();
|
|
1050
|
+
if (waiter) waiter({ value: msg, done: false });
|
|
1051
|
+
else this.items.push(msg);
|
|
1052
|
+
}
|
|
1053
|
+
close() {
|
|
1054
|
+
this.closed = true;
|
|
1055
|
+
for (const w of this.waiters.splice(0)) w({ value: void 0, done: true });
|
|
1056
|
+
}
|
|
1057
|
+
[Symbol.asyncIterator]() {
|
|
1058
|
+
return {
|
|
1059
|
+
next: () => {
|
|
1060
|
+
const item = this.items.shift();
|
|
1061
|
+
if (item) return Promise.resolve({ value: item, done: false });
|
|
1062
|
+
if (this.closed) return Promise.resolve({ value: void 0, done: true });
|
|
1063
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
var allowAll = async (_toolName, input) => ({ behavior: "allow", updatedInput: input });
|
|
1069
|
+
function screenshotFile(cwd, text) {
|
|
1070
|
+
const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
|
|
1071
|
+
if (!file) return null;
|
|
1072
|
+
try {
|
|
1073
|
+
return { data: fs5.readFileSync(path5.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
|
|
1074
|
+
} catch {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
var IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg" };
|
|
1079
|
+
function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
1080
|
+
let names;
|
|
1081
|
+
try {
|
|
1082
|
+
names = fs5.readdirSync(dir);
|
|
1083
|
+
} catch {
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
const found2 = [];
|
|
1087
|
+
for (const name of names.sort()) {
|
|
1088
|
+
const mime = IMAGE_MIME[path5.extname(name).toLowerCase()];
|
|
1089
|
+
const file = path5.join(dir, name);
|
|
1090
|
+
if (!mime || seen.has(name)) continue;
|
|
1091
|
+
try {
|
|
1092
|
+
if (now2 - fs5.statSync(file).mtimeMs < 1e3) continue;
|
|
1093
|
+
} catch {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
found2.push({ name, file, mime });
|
|
1097
|
+
}
|
|
1098
|
+
return found2;
|
|
1099
|
+
}
|
|
1100
|
+
var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
|
|
1101
|
+
function rel(cwd, p) {
|
|
1102
|
+
const s2 = String(p ?? "");
|
|
1103
|
+
if (!s2) return "";
|
|
1104
|
+
const r = path5.relative(cwd, path5.resolve(cwd, s2));
|
|
1105
|
+
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s2;
|
|
1106
|
+
}
|
|
1107
|
+
var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1108
|
+
function tidyPaths(cwd, text) {
|
|
1109
|
+
let out = text;
|
|
1110
|
+
for (const v of /* @__PURE__ */ new Set([cwd, cwd.replaceAll("\\", "/")])) {
|
|
1111
|
+
out = out.replace(new RegExp(`^cd\\s+"?${escapeRe(v)}"?\\s*(&&|;)\\s*`, "i"), "");
|
|
1112
|
+
out = out.replace(new RegExp(`${escapeRe(v)}[\\\\/]?`, "gi"), "./");
|
|
1113
|
+
}
|
|
1114
|
+
return out;
|
|
1115
|
+
}
|
|
1116
|
+
function describeTool(cwd, name, input) {
|
|
1117
|
+
switch (name) {
|
|
1118
|
+
case "Bash":
|
|
1119
|
+
case "PowerShell":
|
|
1120
|
+
return `$ ${clip(tidyPaths(cwd, String(input.command ?? "")).split("\n")[0], 220)}`;
|
|
1121
|
+
case "Read":
|
|
1122
|
+
return `Read ${rel(cwd, input.file_path)}`;
|
|
1123
|
+
case "Write":
|
|
1124
|
+
return `Write ${rel(cwd, input.file_path)}`;
|
|
1125
|
+
case "Edit":
|
|
1126
|
+
case "MultiEdit":
|
|
1127
|
+
return `Edit ${rel(cwd, input.file_path)}`;
|
|
1128
|
+
case "Glob":
|
|
1129
|
+
return `Glob ${input.pattern}`;
|
|
1130
|
+
case "Grep":
|
|
1131
|
+
return `Grep "${clip(String(input.pattern ?? ""), 80)}"`;
|
|
1132
|
+
case "WebFetch":
|
|
1133
|
+
return `Fetch ${input.url}`;
|
|
1134
|
+
case "WebSearch":
|
|
1135
|
+
return `Search "${input.query}"`;
|
|
1136
|
+
case "Task":
|
|
1137
|
+
case "Agent":
|
|
1138
|
+
return `Subagent: ${input.description ?? ""}`;
|
|
1139
|
+
case "TodoWrite":
|
|
1140
|
+
return "Update todo list";
|
|
1141
|
+
case "ToolSearch":
|
|
1142
|
+
return `Load tools ${String(input.query ?? "").replace(/^select:/, "")}`;
|
|
1143
|
+
case "apply_patch": {
|
|
1144
|
+
const files = [...String(input.command ?? "").matchAll(/^\*\*\* (Add|Update|Delete) File: (.+)$/gm)].map((m) => `${m[1] === "Update" ? "" : `${m[1].toLowerCase()} `}${rel(cwd, m[2].trim())}`);
|
|
1145
|
+
return `Edit ${clip(files.join(", ") || "files", 200)}`;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (name.startsWith("mcp__playwright__")) {
|
|
1149
|
+
const action = name.replace("mcp__playwright__browser_", "").replace("mcp__playwright__", "");
|
|
1150
|
+
const detail = input.url ?? input.element ?? input.text ?? "";
|
|
1151
|
+
return `\u{1F310} ${action}${detail ? ` ${clip(String(detail), 120)}` : ""}`;
|
|
1152
|
+
}
|
|
1153
|
+
if (name.startsWith("mcp__office__")) return describeOfficeTool(name.slice("mcp__office__".length), input);
|
|
1154
|
+
return `${name}(${clip(JSON.stringify(input), 140)})`;
|
|
1155
|
+
}
|
|
1156
|
+
function describeOfficeTool(action, input) {
|
|
1157
|
+
const floor = input.floor != null ? ` \u2192 floor ${input.floor}` : "";
|
|
1158
|
+
switch (action) {
|
|
1159
|
+
case "company_status":
|
|
1160
|
+
return "\u{1F3E2} company_status";
|
|
1161
|
+
case "agent_detail":
|
|
1162
|
+
return `\u{1F50E} agent_detail ${String(input.agent_id ?? "")}`;
|
|
1163
|
+
case "set_floor_profile":
|
|
1164
|
+
return `\u{1F5C2}\uFE0F set_floor_profile${floor}${input.summary ? `: ${clip(String(input.summary), 90)}` : ""}`;
|
|
1165
|
+
case "update_job":
|
|
1166
|
+
return `\u{1FAAA} update_job ${input.title ? `"${clip(String(input.title), 60)}"` : String(input.agent_id ?? "")}`;
|
|
1167
|
+
case "propose_hire":
|
|
1168
|
+
return `\u{1F91D} propose_hire ${input.role === "qa" ? "QA " : ""}"${clip(String(input.title ?? ""), 60)}"${floor}`;
|
|
1169
|
+
case "propose_let_go":
|
|
1170
|
+
return `\u{1F44B} propose_let_go ${String(input.agent_id ?? "")}`;
|
|
1171
|
+
case "file_issue":
|
|
1172
|
+
return `\u{1F4DD} file_issue "${clip(String(input.title ?? ""), 70)}"${floor}${input.specialty ? ` \xB7 ${input.specialty}` : ""}`;
|
|
1173
|
+
case "close_issue":
|
|
1174
|
+
return `\u{1F5C2}\uFE0F close_issue #${String(input.number ?? "?")}${floor}`;
|
|
1175
|
+
case "route_issue":
|
|
1176
|
+
return `\u{1F500} route_issue #${String(input.number ?? "?")}${floor}${input.specialty !== void 0 ? ` \xB7 ${input.specialty || "no specialty"}` : ""}${Array.isArray(input.depends_on) ? ` \xB7 depends on ${input.depends_on.map((n) => `#${n}`).join(", ") || "nothing"}` : ""}`;
|
|
1177
|
+
}
|
|
1178
|
+
return `\u{1F3E2} ${action}(${clip(JSON.stringify(input), 120)})`;
|
|
1179
|
+
}
|
|
1180
|
+
function todoLines(input) {
|
|
1181
|
+
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
1182
|
+
return todos.slice(0, 12).map((t) => ({
|
|
1183
|
+
kind: "result",
|
|
1184
|
+
text: ` ${t.status === "completed" ? "\u2612" : t.status === "in_progress" ? "\u25D0" : "\u2610"} ${clip(t.content ?? "", 150)}`
|
|
1185
|
+
}));
|
|
1327
1186
|
}
|
|
1328
|
-
function
|
|
1329
|
-
if (
|
|
1330
|
-
|
|
1331
|
-
|
|
1187
|
+
function resultText(content) {
|
|
1188
|
+
if (typeof content === "string") return content;
|
|
1189
|
+
if (Array.isArray(content)) {
|
|
1190
|
+
return content.filter((b) => b?.type === "text").map((b) => b.text).join("\n");
|
|
1191
|
+
}
|
|
1192
|
+
return "";
|
|
1332
1193
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1194
|
+
var lineCount = (text) => text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "").length;
|
|
1195
|
+
function summariseResult(cwd, tool2, text) {
|
|
1196
|
+
if (tool2 === "Edit" || tool2 === "MultiEdit" || tool2 === "NotebookEdit") return [{ kind: "result", text: " \u23BF Updated" }];
|
|
1197
|
+
if (tool2 === "apply_patch" && /Success\. Updated the following files/.test(text)) return [{ kind: "result", text: " \u23BF Updated" }];
|
|
1198
|
+
if (tool2 === "Write") return [{ kind: "result", text: " \u23BF Saved" }];
|
|
1199
|
+
if (tool2 === "Read") return [{ kind: "result", text: ` \u23BF Read ${lineCount(text)} lines` }];
|
|
1200
|
+
if (tool2 === "Glob") return [{ kind: "result", text: ` \u23BF Found ${lineCount(text)} files` }];
|
|
1201
|
+
const lines = tidyPaths(cwd, text).replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "");
|
|
1202
|
+
if (lines.length === 0) return [{ kind: "result", text: " \u23BF (no output)" }];
|
|
1203
|
+
const shown = lines.slice(0, 3).map((l, i) => ({ kind: "result", text: `${i === 0 ? " \u23BF " : " "}${clip(l, 170)}` }));
|
|
1204
|
+
if (lines.length > 3) shown.push({ kind: "result", text: ` \u2026 +${lines.length - 3} lines` });
|
|
1205
|
+
return shown;
|
|
1335
1206
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1207
|
+
function startSession(opts, callbacks) {
|
|
1208
|
+
let done = false;
|
|
1209
|
+
const cb = {
|
|
1210
|
+
...callbacks,
|
|
1211
|
+
finished: (r) => {
|
|
1212
|
+
if (done) return;
|
|
1213
|
+
done = true;
|
|
1214
|
+
callbacks.finished(r);
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
const input = new InputQueue();
|
|
1218
|
+
const abort = new AbortController();
|
|
1219
|
+
let pendingTurns = 1;
|
|
1220
|
+
let stopped = false;
|
|
1221
|
+
let lastCost = 0;
|
|
1222
|
+
let lastTurns = 0;
|
|
1223
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
1224
|
+
input.push(opts.prompt);
|
|
1225
|
+
const env = {};
|
|
1226
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1227
|
+
if (/^(ANTHROPIC_|CLAUDE)/i.test(k) && k !== "CLAUDE_CONFIG_DIR") continue;
|
|
1228
|
+
env[k] = v;
|
|
1353
1229
|
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
if (
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1230
|
+
env.CLAUDE_AGENT_SDK_CLIENT_APP = `cubefarm/${VERSION}`;
|
|
1231
|
+
const mcpServers = {};
|
|
1232
|
+
if (opts.browserTesting) {
|
|
1233
|
+
const browser = playwrightMcp();
|
|
1234
|
+
if (browser) mcpServers.playwright = browser;
|
|
1235
|
+
const problem = browserProblem(browser);
|
|
1236
|
+
if (problem) cb.log([{ kind: "error", text: problem }]);
|
|
1237
|
+
}
|
|
1238
|
+
if (opts.office) mcpServers.office = opts.office.server;
|
|
1239
|
+
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", ...CROSS_TURN_TOOLS];
|
|
1240
|
+
if (opts.role === "ceo") disallowedTools.push("Bash", "PowerShell", "NotebookEdit");
|
|
1241
|
+
const options = {
|
|
1242
|
+
cwd: opts.cwd,
|
|
1243
|
+
additionalDirectories: opts.additionalDirectories,
|
|
1244
|
+
abortController: abort,
|
|
1245
|
+
env,
|
|
1246
|
+
model: opts.model || void 0,
|
|
1247
|
+
effort: opts.effort,
|
|
1248
|
+
// The manager's own setup (settings, skills, plugins, MCP servers, connectors), plus the office's servers below.
|
|
1249
|
+
settingSources: ["user", "project", "local"],
|
|
1250
|
+
systemPrompt: { type: "preset", preset: "claude_code", append: opts.systemAppend },
|
|
1251
|
+
mcpServers,
|
|
1252
|
+
disallowedTools,
|
|
1253
|
+
resume: opts.resumeSessionId,
|
|
1254
|
+
stderr: (data) => {
|
|
1255
|
+
const line = data.trim();
|
|
1256
|
+
if (line && /error|fail/i.test(line)) cb.log([{ kind: "error", text: clip(line, 200) }]);
|
|
1257
|
+
},
|
|
1258
|
+
permissionMode: "acceptEdits",
|
|
1259
|
+
canUseTool: allowAll,
|
|
1260
|
+
...opts.outputSchema ? { outputFormat: { type: "json_schema", schema: opts.outputSchema } } : {}
|
|
1261
|
+
};
|
|
1262
|
+
let emptyTurn;
|
|
1263
|
+
const answered = (msg) => {
|
|
1264
|
+
pendingTurns -= 1;
|
|
1265
|
+
if (pendingTurns > 0) return;
|
|
1266
|
+
input.close();
|
|
1267
|
+
const ok = msg.subtype === "success" && !msg.is_error;
|
|
1268
|
+
cb.finished({
|
|
1269
|
+
ok,
|
|
1270
|
+
text: msg.subtype === "success" ? msg.result : "",
|
|
1271
|
+
costUsd: lastCost,
|
|
1272
|
+
turns: lastTurns,
|
|
1273
|
+
errors: msg.subtype === "success" ? [] : msg.errors,
|
|
1274
|
+
structured: msg.subtype === "success" ? msg.structured_output : void 0
|
|
1275
|
+
});
|
|
1276
|
+
};
|
|
1277
|
+
const handle = (msg) => {
|
|
1278
|
+
clearTimeout(emptyTurn);
|
|
1279
|
+
if (done && msg.type === "assistant" && !stopped) {
|
|
1280
|
+
stopped = true;
|
|
1281
|
+
cb.log([{ kind: "system", text: "\u25A0 This session kept working after it had finished, so it was stopped." }]);
|
|
1282
|
+
abort.abort();
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
switch (msg.type) {
|
|
1286
|
+
case "system": {
|
|
1287
|
+
if (msg.subtype === "init") {
|
|
1288
|
+
cb.sessionId(msg.session_id);
|
|
1289
|
+
const mcp = msg.mcp_servers.map((s2) => `${s2.name}:${s2.status}`).join(", ");
|
|
1290
|
+
cb.log([
|
|
1291
|
+
{ kind: "system", text: `\u273B Claude Code ${msg.claude_code_version} \xB7 ${msg.model} \xB7 ${msg.effort ?? opts.effort} effort` },
|
|
1292
|
+
{ kind: "system", text: ` cwd ${msg.cwd}${mcp ? ` \xB7 mcp ${mcp}` : ""}` }
|
|
1293
|
+
]);
|
|
1294
|
+
} else if (msg.subtype === "api_retry") {
|
|
1295
|
+
cb.log([{ kind: "system", text: `\u2026 API retry ${msg.attempt}/${msg.max_retries} in ${Math.round(msg.retry_delay_ms / 1e3)}s` }]);
|
|
1296
|
+
} else if (msg.subtype === "compact_boundary") {
|
|
1297
|
+
cb.log([{ kind: "system", text: "\u273B Conversation compacted" }]);
|
|
1298
|
+
}
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
case "rate_limit_event": {
|
|
1302
|
+
const info = msg.rate_limit_info;
|
|
1303
|
+
if (info.status !== "allowed") {
|
|
1304
|
+
const when = info.resetsAt ? new Date(info.resetsAt * 1e3).toLocaleTimeString() : "later";
|
|
1305
|
+
cb.log([{ kind: "error", text: `\u26A0 Subscription usage ${info.status === "rejected" ? "limit reached" : "warning"} (${info.rateLimitType ?? "limit"}) \xB7 resets ${when}` }]);
|
|
1306
|
+
const overage = info.isUsingOverage || info.overageStatus === "allowed" || info.overageStatus === "allowed_warning";
|
|
1307
|
+
if (info.status === "rejected" && !overage) cb.limited?.(info.resetsAt ? info.resetsAt * 1e3 : null);
|
|
1308
|
+
if (info.status === "allowed_warning" && !overage) {
|
|
1309
|
+
cb.usageWarning?.({ resetsAt: info.resetsAt ? info.resetsAt * 1e3 : null, rateLimitType: info.rateLimitType ?? null, utilization: info.utilization ?? null });
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
case "assistant": {
|
|
1315
|
+
if (msg.parent_tool_use_id) break;
|
|
1316
|
+
const out = [];
|
|
1317
|
+
for (const block of msg.message.content) {
|
|
1318
|
+
if (block.type === "text" && block.text.trim()) {
|
|
1319
|
+
const lines = block.text.trim().split("\n");
|
|
1320
|
+
lines.slice(0, 30).forEach((l, i) => out.push({ kind: "text", text: `${i === 0 ? "\u25CF " : " "}${l}` }));
|
|
1321
|
+
if (lines.length > 30) out.push({ kind: "text", text: ` \u2026 (${lines.length - 30} more lines)` });
|
|
1322
|
+
} else if (block.type === "thinking") {
|
|
1323
|
+
out.push({ kind: "thinking", text: "\u273B Thinking\u2026" });
|
|
1324
|
+
} else if (block.type === "tool_use") {
|
|
1325
|
+
const toolInput2 = block.input ?? {};
|
|
1326
|
+
toolNames.set(block.id, block.name);
|
|
1327
|
+
out.push({ kind: "tool", tool: block.name, text: `\u23FA ${describeTool(opts.cwd, block.name, toolInput2)}` });
|
|
1328
|
+
if (block.name === "TodoWrite") out.push(...todoLines(toolInput2));
|
|
1329
|
+
cb.tool(block.name);
|
|
1330
|
+
if (block.name === "mcp__playwright__browser_navigate" && typeof toolInput2.url === "string") cb.browserUrl(toolInput2.url);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (out.length) cb.log(out);
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
case "user": {
|
|
1337
|
+
if (msg.parent_tool_use_id) break;
|
|
1338
|
+
const content = msg.message.content;
|
|
1339
|
+
if (!Array.isArray(content)) break;
|
|
1340
|
+
const out = [];
|
|
1341
|
+
for (const block of content) {
|
|
1342
|
+
if (block.type !== "tool_result") continue;
|
|
1343
|
+
const name = toolNames.get(block.tool_use_id) ?? "";
|
|
1344
|
+
const text = resultText(block.content);
|
|
1345
|
+
let images = 0;
|
|
1346
|
+
if (Array.isArray(block.content)) {
|
|
1347
|
+
for (const part of block.content) {
|
|
1348
|
+
if (part.type === "image" && part.source.type === "base64") {
|
|
1349
|
+
images += 1;
|
|
1350
|
+
cb.screenshot(Buffer.from(part.source.data, "base64"), part.source.media_type);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
const saved = name === "mcp__playwright__browser_take_screenshot" && !images ? screenshotFile(opts.cwd, text) : null;
|
|
1355
|
+
if (saved) cb.screenshot(saved.data, saved.mime);
|
|
1356
|
+
if (name.startsWith("mcp__playwright__")) {
|
|
1357
|
+
const url = text.match(/Page URL:\s*(\S+)/);
|
|
1358
|
+
if (url) cb.browserUrl(url[1]);
|
|
1359
|
+
}
|
|
1360
|
+
if (name === "TodoWrite") continue;
|
|
1361
|
+
if (block.is_error) out.push({ kind: "error", text: ` \u23BF ${clip(tidyPaths(opts.cwd, text).split("\n")[0] || "Error", 200)}` });
|
|
1362
|
+
else out.push(...summariseResult(opts.cwd, name, text));
|
|
1363
|
+
}
|
|
1364
|
+
cb.tool(null);
|
|
1365
|
+
if (out.length) cb.log(out);
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
case "result": {
|
|
1369
|
+
lastCost = msg.total_cost_usd;
|
|
1370
|
+
lastTurns = msg.num_turns;
|
|
1371
|
+
if (msg.subtype === "success" && !msg.is_error && msg.num_turns === 0) {
|
|
1372
|
+
emptyTurn = setTimeout(() => answered(msg), 3e4);
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
if (msg.subtype === "success" && !msg.is_error && msg.result.trim()) cb.turn?.(msg.result.trim());
|
|
1376
|
+
answered(msg);
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1397
1379
|
}
|
|
1398
1380
|
};
|
|
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) : '' });
|
|
1381
|
+
(async () => {
|
|
1382
|
+
try {
|
|
1383
|
+
for await (const msg of query({ prompt: input, options })) handle(msg);
|
|
1384
|
+
if (pendingTurns > 0) {
|
|
1385
|
+
cb.finished({ ok: false, text: "", costUsd: lastCost, turns: lastTurns, errors: [stopped ? "Stopped by manager" : "Session ended unexpectedly"] });
|
|
1406
1386
|
}
|
|
1407
|
-
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
input.close();
|
|
1389
|
+
const message = stopped ? "Stopped by manager" : err instanceof Error ? err.message : String(err);
|
|
1390
|
+
cb.finished({ ok: false, text: "", costUsd: lastCost, turns: lastTurns, errors: [message] });
|
|
1391
|
+
}
|
|
1392
|
+
})();
|
|
1393
|
+
return {
|
|
1394
|
+
send(text) {
|
|
1395
|
+
pendingTurns += 1;
|
|
1396
|
+
input.push(text);
|
|
1408
1397
|
},
|
|
1398
|
+
stop() {
|
|
1399
|
+
stopped = true;
|
|
1400
|
+
clearTimeout(emptyTurn);
|
|
1401
|
+
input.close();
|
|
1402
|
+
abort.abort();
|
|
1403
|
+
}
|
|
1409
1404
|
};
|
|
1410
1405
|
}
|
|
1411
|
-
|
|
1406
|
+
|
|
1407
|
+
// server/cliRunner.ts
|
|
1408
|
+
import crypto3 from "node:crypto";
|
|
1409
|
+
import fs7 from "node:fs";
|
|
1410
|
+
import path8 from "node:path";
|
|
1411
|
+
import { pathToFileURL } from "node:url";
|
|
1412
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1412
1413
|
|
|
1413
1414
|
// server/ptyClient.ts
|
|
1414
1415
|
import crypto2 from "node:crypto";
|
|
@@ -4021,6 +4022,101 @@ function holdUps(issues) {
|
|
|
4021
4022
|
return out;
|
|
4022
4023
|
}
|
|
4023
4024
|
|
|
4025
|
+
// server/prompts.ts
|
|
4026
|
+
var PLACEHOLDERS = {
|
|
4027
|
+
worktree: "<your worktree>",
|
|
4028
|
+
issue: "<issue>",
|
|
4029
|
+
pr: "<PR>",
|
|
4030
|
+
prTitle: "<PR title>",
|
|
4031
|
+
prUrl: "<PR URL>",
|
|
4032
|
+
branch: "<branch>"
|
|
4033
|
+
};
|
|
4034
|
+
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.";
|
|
4035
|
+
var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
|
|
4036
|
+
var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
|
|
4037
|
+
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing, depsLine }) {
|
|
4038
|
+
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
4039
|
+
const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
|
|
4040
|
+
return [
|
|
4041
|
+
`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.`,
|
|
4042
|
+
`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.`,
|
|
4043
|
+
a.brief ? `
|
|
4044
|
+
Your job description:
|
|
4045
|
+
${a.brief}` : "",
|
|
4046
|
+
"",
|
|
4047
|
+
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
4048
|
+
repo.summary ? `Project: ${repo.summary}` : "",
|
|
4049
|
+
repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
|
|
4050
|
+
`Your worktree: ${cwd}`,
|
|
4051
|
+
depsLine ?? "",
|
|
4052
|
+
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})`,
|
|
4053
|
+
links.length ? `Related repositories you may read for context (do not modify them):
|
|
4054
|
+
${links.join("\n")}` : "",
|
|
4055
|
+
"",
|
|
4056
|
+
"Workflow:",
|
|
4057
|
+
"1. Read the issue and explore the relevant code before changing anything.",
|
|
4058
|
+
"2. Implement the change with focused commits and clear messages.",
|
|
4059
|
+
"3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed.",
|
|
4060
|
+
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.",
|
|
4061
|
+
`5. Push: ${push}`,
|
|
4062
|
+
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>".`,
|
|
4063
|
+
fixing ? "" : "7. End your final message with the pull request URL on its own line.",
|
|
4064
|
+
"",
|
|
4065
|
+
"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.",
|
|
4066
|
+
ONE_TURN
|
|
4067
|
+
].filter((l) => l !== "").join("\n");
|
|
4068
|
+
}
|
|
4069
|
+
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, depsLine, testStep: testStep2 }) {
|
|
4070
|
+
return [
|
|
4071
|
+
`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.`,
|
|
4072
|
+
...a.brief ? ["", `Your job description:
|
|
4073
|
+
${a.brief}`] : [],
|
|
4074
|
+
...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."] : [],
|
|
4075
|
+
"",
|
|
4076
|
+
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
4077
|
+
...repo.summary ? [`Project: ${repo.summary}`] : [],
|
|
4078
|
+
...repo.mission ? [`What the team is building (the manager's brief): ${repo.mission}`] : [],
|
|
4079
|
+
...repo.qaBrief ? [`What to check on this project (from the CEO):
|
|
4080
|
+
${repo.qaBrief}`] : [],
|
|
4081
|
+
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
4082
|
+
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
4083
|
+
...depsLine ? [depsLine] : [],
|
|
4084
|
+
"",
|
|
4085
|
+
"How to test:",
|
|
4086
|
+
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
4087
|
+
`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.`,
|
|
4088
|
+
testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4089
|
+
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).",
|
|
4090
|
+
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
4091
|
+
"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.",
|
|
4092
|
+
"",
|
|
4093
|
+
"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.",
|
|
4094
|
+
ONE_TURN
|
|
4095
|
+
].join("\n");
|
|
4096
|
+
}
|
|
4097
|
+
var OFFICE = "Office instructions";
|
|
4098
|
+
var MARK = "\0job description\0";
|
|
4099
|
+
function promptParts(build, brief) {
|
|
4100
|
+
if (!brief) return [{ label: OFFICE, text: build(""), editable: false }];
|
|
4101
|
+
const [before, after] = build(MARK).split(MARK);
|
|
4102
|
+
return [
|
|
4103
|
+
{ label: OFFICE, text: before, editable: false },
|
|
4104
|
+
{ label: "Job description", text: brief, editable: true },
|
|
4105
|
+
{ label: OFFICE, text: after, editable: false }
|
|
4106
|
+
].filter((p) => p.text !== "");
|
|
4107
|
+
}
|
|
4108
|
+
var view = (kind, parts) => ({ kind, text: parts.map((p) => p.text).join(""), parts });
|
|
4109
|
+
function devPromptPreview({ slug, ...i }) {
|
|
4110
|
+
const branch = devBranch(PLACEHOLDERS.issue, slug);
|
|
4111
|
+
return view("dev", promptParts((brief) => devSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch }), i.agent.brief));
|
|
4112
|
+
}
|
|
4113
|
+
function qaPromptPreview({ slug, ...i }) {
|
|
4114
|
+
const pr = { number: PLACEHOLDERS.pr, title: PLACEHOLDERS.prTitle, headRefName: PLACEHOLDERS.branch, url: PLACEHOLDERS.prUrl };
|
|
4115
|
+
const branch = qaBranch(PLACEHOLDERS.pr, slug);
|
|
4116
|
+
return view("qa", promptParts((brief) => qaSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch, pr }), i.agent.brief));
|
|
4117
|
+
}
|
|
4118
|
+
var ceoPromptPreview = (text) => view("ceo", [{ label: OFFICE, text, editable: false }]);
|
|
4119
|
+
|
|
4024
4120
|
// server/ceo.ts
|
|
4025
4121
|
var EFFORT = z2.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
4026
4122
|
function createOfficeTools(h) {
|
|
@@ -4161,6 +4257,7 @@ function ceoSystemPrompt(o) {
|
|
|
4161
4257
|
"- Read the repositories through their clone paths with Read, Glob and Grep. They are read-only to you. You cannot run shell commands.",
|
|
4162
4258
|
`- 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
4259
|
"- Change things only through the mcp__office__ tools.",
|
|
4260
|
+
`- ${ONE_TURN}`,
|
|
4164
4261
|
"- 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
4262
|
"",
|
|
4166
4263
|
"Rules:",
|
|
@@ -4639,97 +4736,13 @@ var Previews = class {
|
|
|
4639
4736
|
}
|
|
4640
4737
|
};
|
|
4641
4738
|
|
|
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));
|
|
4739
|
+
// server/qaRetry.ts
|
|
4740
|
+
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.";
|
|
4741
|
+
function qaRetry(e) {
|
|
4742
|
+
if (e.stopped) return "give-up";
|
|
4743
|
+
if (e.report) return "report";
|
|
4744
|
+
return e.ok && !e.limited && e.canResume ? "resume" : "give-up";
|
|
4726
4745
|
}
|
|
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));
|
|
4731
|
-
}
|
|
4732
|
-
var ceoPromptPreview = (text) => view("ceo", [{ label: OFFICE, text, editable: false }]);
|
|
4733
4746
|
|
|
4734
4747
|
// server/pacing.ts
|
|
4735
4748
|
var PACING_MS = 60 * 6e4;
|
|
@@ -6612,13 +6625,24 @@ Note from the manager: ${note}` : ""
|
|
|
6612
6625
|
Linked issue: ${issue2.title}
|
|
6613
6626
|
${issue2.body.trim() || "(no description)"}` : ""
|
|
6614
6627
|
].filter((l) => l !== "").join("\n");
|
|
6615
|
-
|
|
6628
|
+
const systemAppend = this.buildQaSystemAppend(a, repo, cwd, branch, pr, qa.testStep, desk.deps);
|
|
6629
|
+
this.agentRt.get(a.id).qaResume = { cwd, systemAppend };
|
|
6630
|
+
this.startAgentSession(a, repo, cwd, prompt, systemAppend, void 0, QA_SCHEMA);
|
|
6616
6631
|
}
|
|
6617
6632
|
async onQaFinished(a, repo, result) {
|
|
6618
6633
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
6619
6634
|
const rt = this.agentRt.get(a.id);
|
|
6620
6635
|
const report = result.ok ? parseReport(result) : null;
|
|
6621
|
-
|
|
6636
|
+
const resume = rt.qaResume;
|
|
6637
|
+
const next = qaRetry({ stopped: a.status === "stopped", limited: this.limited(), ok: result.ok, report: !!report, canResume: !!resume && !!a.sessionId });
|
|
6638
|
+
if (next === "resume" && resume && a.sessionId) {
|
|
6639
|
+
rt.qaResume = null;
|
|
6640
|
+
a.endedAt = null;
|
|
6641
|
+
this.appendLog(a, [{ kind: "system", text: "\u21BB The session ended without a QA report. Resuming it once to finish." }]);
|
|
6642
|
+
this.startAgentSession(a, repo, resume.cwd, QA_RESUME_PROMPT, resume.systemAppend, a.sessionId, QA_SCHEMA);
|
|
6643
|
+
return;
|
|
6644
|
+
}
|
|
6645
|
+
if (next === "give-up" || !report) {
|
|
6622
6646
|
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
6647
|
if (rec) {
|
|
6624
6648
|
const failures = rec.sessionFailures + (this.limited() ? 0 : 1);
|
|
@@ -6637,17 +6661,17 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
6637
6661
|
let commentUrl = null;
|
|
6638
6662
|
if (rec) {
|
|
6639
6663
|
const pull = !pass ? await this.backend.prDetails(repo.fullName, rec.prNumber).catch(() => null) : null;
|
|
6640
|
-
const
|
|
6664
|
+
const next2 = qaOutcome(pass, rec, pull);
|
|
6641
6665
|
try {
|
|
6642
6666
|
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,
|
|
6667
|
+
const body2 = await this.renderQaComment(a, repo, rec, report, rt.shots, next2);
|
|
6644
6668
|
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber, body2) || null;
|
|
6645
6669
|
this.appendLog(a, [{ kind: "system", text: ` \u23BF ${commentUrl ?? "comment posted"}` }]);
|
|
6646
6670
|
} catch (err) {
|
|
6647
6671
|
this.appendLog(a, [{ kind: "error", text: ` \u23BF Could not post the QA report: ${err.message}` }]);
|
|
6648
6672
|
}
|
|
6649
6673
|
const qaRounds = rec.round - rec.retests;
|
|
6650
|
-
const nextStatus =
|
|
6674
|
+
const nextStatus = next2 === "conflict" ? "failed" : next2;
|
|
6651
6675
|
const fixInstructions = report.fixInstructions ?? report.checks.filter((c) => c.result === "fail").map((c) => `${c.name}: ${c.details}`).join("\n");
|
|
6652
6676
|
this.setQa(rec, {
|
|
6653
6677
|
status: nextStatus,
|
|
@@ -6664,10 +6688,10 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
6664
6688
|
alerted: false,
|
|
6665
6689
|
qaChecks: pull?.checks ?? null
|
|
6666
6690
|
});
|
|
6667
|
-
if (
|
|
6691
|
+
if (next2 === "conflict") this.sendBack(repo, rec, "conflict", conflictFixInstructions(fixInstructions, repo.defaultBranch), false);
|
|
6668
6692
|
this.toast(
|
|
6669
6693
|
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` :
|
|
6694
|
+
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
6695
|
);
|
|
6672
6696
|
}
|
|
6673
6697
|
}
|