cubefarm 0.3.6 → 0.3.8
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/bin/cubefarm.js +9 -1
- package/dist/assets/ToyWorld-DAEljc3S.js +2 -0
- package/dist/assets/{index-CvwxmawV.js → index-B7sInPPW.js} +68 -68
- package/dist/assets/index-CIr98Zaa.css +1 -0
- package/dist/assets/{rapier-DS9bMu-p.js → rapier-sDtDRX5B.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +544 -211
- package/dist-server/index.js.map +4 -4
- package/package.json +3 -1
- package/dist/assets/ToyWorld-C1nzIgVK.js +0 -2
- package/dist/assets/index-BU33ZUhQ.css +0 -1
package/dist-server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// server/index.ts
|
|
2
2
|
import http2 from "node:http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import path14 from "node:path";
|
|
4
|
+
import fs12 from "node:fs";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
|
|
@@ -17,6 +17,7 @@ var DEMO = process.argv.includes("--demo") || process.env.SWARM_DEMO === "1" ||
|
|
|
17
17
|
var STATE_FILE = path.join(HOME_DIR, DEMO ? "demo-state.json" : "state.json");
|
|
18
18
|
var SYNC_INTERVAL_MS = 45e3;
|
|
19
19
|
var SCHEDULER_INTERVAL_MS = 8e3;
|
|
20
|
+
var DESK_SWEEP_INTERVAL_MS = 30 * 6e4;
|
|
20
21
|
var LOG_BUFFER = 600;
|
|
21
22
|
function defaultProjectsDir(appDir, home = os.homedir()) {
|
|
22
23
|
if (!appDir.split(/[\\/]/).includes("node_modules")) return path.resolve(appDir, "..");
|
|
@@ -162,6 +163,9 @@ async function issueState(fullName, number) {
|
|
|
162
163
|
return null;
|
|
163
164
|
}
|
|
164
165
|
}
|
|
166
|
+
async function closeIssue(fullName, number, comment) {
|
|
167
|
+
await gh(["issue", "close", String(number), "-R", fullName, "--reason", "completed", "--comment", comment]);
|
|
168
|
+
}
|
|
165
169
|
async function editIssue(fullName, number, edit) {
|
|
166
170
|
const args = ["issue", "edit", String(number), "-R", fullName];
|
|
167
171
|
if (edit.body !== void 0) args.push("--body-file", "-");
|
|
@@ -296,6 +300,33 @@ function npmInstall(dir) {
|
|
|
296
300
|
installs.set(dir, next);
|
|
297
301
|
return next;
|
|
298
302
|
}
|
|
303
|
+
function porcelainPaths(porcelain) {
|
|
304
|
+
return porcelain.split(/\r?\n/).map((line) => /^([ MTADRCU?!]{1,2}) (.+)$/.exec(line)).filter((m) => m !== null).map(([, xy, file]) => /[RC]/.test(xy) ? /^(?:"(?:[^"\\]|\\.)*"|.*?) -> (.+)$/.exec(file)?.[1] ?? file : file);
|
|
305
|
+
}
|
|
306
|
+
function overwrittenPaths(stderr) {
|
|
307
|
+
const lines = stderr.split(/\r?\n/);
|
|
308
|
+
const start = lines.findIndex((l) => /would be overwritten by/.test(l));
|
|
309
|
+
if (start < 0) return [];
|
|
310
|
+
const files = [];
|
|
311
|
+
for (const line of lines.slice(start + 1)) {
|
|
312
|
+
if (!line.startsWith(" ")) break;
|
|
313
|
+
files.push(line.trim());
|
|
314
|
+
}
|
|
315
|
+
return files.filter(Boolean);
|
|
316
|
+
}
|
|
317
|
+
function fileList(files, max = 3, maxChars = 120) {
|
|
318
|
+
const shown = [];
|
|
319
|
+
let length = 0;
|
|
320
|
+
for (const file of files.slice(0, max)) {
|
|
321
|
+
const name = file.length > maxChars ? `\u2026${file.slice(-(maxChars - 1))}` : file;
|
|
322
|
+
const add = name.length + (shown.length ? 2 : 0);
|
|
323
|
+
if (shown.length && length + add > maxChars) break;
|
|
324
|
+
shown.push(name);
|
|
325
|
+
length += add;
|
|
326
|
+
}
|
|
327
|
+
const more = files.length - shown.length;
|
|
328
|
+
return `${shown.join(", ")}${more ? ` +${more} more` : ""}`;
|
|
329
|
+
}
|
|
299
330
|
async function syncMain(fullName, defaultBranch, opts) {
|
|
300
331
|
const dir = mainDir(fullName);
|
|
301
332
|
const result = await withRepoLock(fullName, async () => {
|
|
@@ -313,12 +344,17 @@ async function syncMain(fullName, defaultBranch, opts) {
|
|
|
313
344
|
if (behind === 0) return stays("in sync");
|
|
314
345
|
if (Number(await g(["rev-list", "--count", `${target}..HEAD`])) > 0) return stays(`diverged: local commits, and ${commits} to pull`);
|
|
315
346
|
if (!opts.touch) return { status: `update ready (${commits})`, behind, updatable: true };
|
|
316
|
-
|
|
347
|
+
const dirty = await g(["status", "--porcelain", "--untracked-files=no"]);
|
|
348
|
+
if (dirty) {
|
|
349
|
+
const files = porcelainPaths(dirty);
|
|
350
|
+
return { status: `${behind} behind: local changes${files.length ? ` in ${fileList(files)}` : ""}`, behind, updatable: true };
|
|
351
|
+
}
|
|
317
352
|
const before = await g(["rev-parse", "HEAD"]);
|
|
318
353
|
try {
|
|
319
354
|
await g(["merge", "--ff-only", target], 12e4);
|
|
320
|
-
} catch {
|
|
321
|
-
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const files = overwrittenPaths(err.stderr ?? "");
|
|
357
|
+
return { status: `${behind} behind: local files are in the way${files.length ? `: ${fileList(files)}` : ""}`, behind, updatable: true };
|
|
322
358
|
}
|
|
323
359
|
const changed = (await g(["diff", "--name-only", before, "HEAD"])).split(/\r?\n/);
|
|
324
360
|
const install = changed.some((f) => f === "package.json" || f === "package-lock.json") && await exists(path2.join(dir, "package.json"));
|
|
@@ -463,9 +499,8 @@ function prepareDesk(fullName, base, agentSlug, branch) {
|
|
|
463
499
|
return wt;
|
|
464
500
|
});
|
|
465
501
|
}
|
|
466
|
-
function removeDesk(fullName, agentSlug) {
|
|
502
|
+
function removeDesk(fullName, agentSlug, main = mainDir(fullName)) {
|
|
467
503
|
return withRepoLock(fullName, async () => {
|
|
468
|
-
const main = mainDir(fullName);
|
|
469
504
|
const wt = deskDir(fullName, agentSlug);
|
|
470
505
|
if (!await exists(wt)) return;
|
|
471
506
|
await git(["worktree", "remove", "--force", wt], { cwd: main }).catch(() => void 0);
|
|
@@ -473,6 +508,121 @@ function removeDesk(fullName, agentSlug) {
|
|
|
473
508
|
await git(["worktree", "prune"], { cwd: main }).catch(() => void 0);
|
|
474
509
|
});
|
|
475
510
|
}
|
|
511
|
+
var OLD_HOMES = [".office-swarm"];
|
|
512
|
+
var OFFICE_BRANCH = /^(swarm\/issue-|qa\/pr-)/;
|
|
513
|
+
function parseWorktrees(porcelain) {
|
|
514
|
+
const out = [];
|
|
515
|
+
for (const line of porcelain.split(/\r?\n/)) {
|
|
516
|
+
if (line.startsWith("worktree ")) out.push({ path: path2.resolve(line.slice(9)), branch: null, locked: false });
|
|
517
|
+
else if (!out.length) continue;
|
|
518
|
+
else if (line.startsWith("branch ")) out[out.length - 1].branch = line.slice(7).replace(/^refs\/heads\//, "");
|
|
519
|
+
else if (line === "locked" || line.startsWith("locked ")) out[out.length - 1].locked = true;
|
|
520
|
+
}
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
var samePath = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
524
|
+
function isOfficeDesk(p, fullName, root) {
|
|
525
|
+
const desks = path2.dirname(p);
|
|
526
|
+
const repo = path2.dirname(desks);
|
|
527
|
+
const workspaces = path2.dirname(repo);
|
|
528
|
+
if (!samePath(path2.basename(desks), "desks") || !samePath(path2.basename(repo), fullName.replace("/", "__"))) return false;
|
|
529
|
+
if (samePath(workspaces, path2.resolve(root))) return true;
|
|
530
|
+
return path2.basename(workspaces) === "workspaces" && OLD_HOMES.some((h) => samePath(path2.basename(path2.dirname(workspaces)), h));
|
|
531
|
+
}
|
|
532
|
+
function planSweep(input, keep) {
|
|
533
|
+
const deskRoot = path2.join(path2.resolve(input.root), input.fullName.replace("/", "__"), "desks");
|
|
534
|
+
const kept = (p) => keep.desks.some((slug) => samePath(p, path2.join(deskRoot, slug)));
|
|
535
|
+
const worktrees = input.worktrees.slice(1).filter((w) => !w.locked && isOfficeDesk(w.path, input.fullName, input.root) && !kept(w.path));
|
|
536
|
+
const stays = input.worktrees.filter((w) => !worktrees.includes(w));
|
|
537
|
+
const folders = input.folders.filter((name) => {
|
|
538
|
+
const p = path2.join(deskRoot, name);
|
|
539
|
+
return !kept(p) && !input.worktrees.some((w) => samePath(w.path, p));
|
|
540
|
+
});
|
|
541
|
+
const branches = input.branches.filter((b) => OFFICE_BRANCH.test(b) && !keep.branches.includes(b) && !stays.some((w) => w.branch === b));
|
|
542
|
+
return { worktrees, folders, branches };
|
|
543
|
+
}
|
|
544
|
+
async function inUse(dir) {
|
|
545
|
+
const probe = `${dir}.sweep`;
|
|
546
|
+
try {
|
|
547
|
+
await fs2.rename(dir, probe);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
return err.code !== "ENOENT";
|
|
550
|
+
}
|
|
551
|
+
await fs2.rename(probe, dir);
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
var stamp = (d) => `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
|
|
555
|
+
async function saveLeftovers(fullName, desk) {
|
|
556
|
+
const g = (args) => git(args, { cwd: desk, timeoutMs: 6e4 });
|
|
557
|
+
if (!await g(["rev-parse", "--verify", "-q", "HEAD"]).catch(() => "")) return null;
|
|
558
|
+
const dirty = await g(["status", "--porcelain", "--untracked-files=no"]);
|
|
559
|
+
const commits = Number(await g(["rev-list", "--count", "HEAD", "--not", "--remotes"]));
|
|
560
|
+
if (!dirty && !commits) return null;
|
|
561
|
+
const dir = path2.join(HOME_DIR, "leftovers", fullName.replace("/", "__"));
|
|
562
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
563
|
+
const tmp = await fs2.mkdtemp(path2.join(dir, ".tmp-"));
|
|
564
|
+
try {
|
|
565
|
+
if (commits) await g(["format-patch", "-q", "-o", tmp, "HEAD", "--not", "--remotes"]);
|
|
566
|
+
if (dirty) await g(["diff", "HEAD", "--binary", `--output=${path2.join(tmp, "zz-uncommitted.patch")}`]);
|
|
567
|
+
const parts = (await fs2.readdir(tmp)).sort();
|
|
568
|
+
const base = `${path2.basename(desk)}-${stamp(/* @__PURE__ */ new Date())}`;
|
|
569
|
+
let file = path2.join(dir, `${base}.patch`);
|
|
570
|
+
for (let n = 2; await exists(file); n++) file = path2.join(dir, `${base}-${n}.patch`);
|
|
571
|
+
await fs2.writeFile(file, Buffer.concat(await Promise.all(parts.map((p) => fs2.readFile(path2.join(tmp, p))))));
|
|
572
|
+
return file;
|
|
573
|
+
} finally {
|
|
574
|
+
await removeDir(tmp).catch(() => void 0);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function sweepDesks(fullName, keep) {
|
|
578
|
+
return withRepoLock(fullName, async () => {
|
|
579
|
+
const result = { desks: 0, folders: 0, branches: 0, patches: [], skipped: [] };
|
|
580
|
+
const main = mainDir(fullName);
|
|
581
|
+
if (!await exists(path2.join(main, ".git"))) return result;
|
|
582
|
+
const g = (args) => git(args, { cwd: main, timeoutMs: 6e4 });
|
|
583
|
+
await g(["worktree", "prune"]);
|
|
584
|
+
const real = (p) => fs2.realpath(p).catch(() => p);
|
|
585
|
+
const worktrees = await Promise.all(parseWorktrees(await g(["worktree", "list", "--porcelain"])).map(async (w) => ({ ...w, path: await real(w.path) })));
|
|
586
|
+
const refs = await g(["for-each-ref", "--format=%(refname)", "refs/heads/swarm", "refs/heads/qa"]);
|
|
587
|
+
const branches = refs.split(/\r?\n/).filter(Boolean).map((r) => r.replace(/^refs\/heads\//, ""));
|
|
588
|
+
const root = await real(WORKSPACE_ROOT);
|
|
589
|
+
const deskRoot = path2.join(root, fullName.replace("/", "__"), "desks");
|
|
590
|
+
const folders = (await fs2.readdir(deskRoot, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
591
|
+
const plan = planSweep({ fullName, root, worktrees, branches, folders }, keep);
|
|
592
|
+
const held = /* @__PURE__ */ new Set();
|
|
593
|
+
for (const wt of plan.worktrees) {
|
|
594
|
+
try {
|
|
595
|
+
if (await inUse(wt.path)) throw new Error("in use");
|
|
596
|
+
const patch = await saveLeftovers(fullName, wt.path);
|
|
597
|
+
if (patch) result.patches.push(patch);
|
|
598
|
+
await g(["worktree", "remove", "--force", wt.path]).catch(() => void 0);
|
|
599
|
+
await removeDir(wt.path);
|
|
600
|
+
result.desks++;
|
|
601
|
+
} catch {
|
|
602
|
+
result.skipped.push(wt.path);
|
|
603
|
+
if (wt.branch) held.add(wt.branch);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
await g(["worktree", "prune"]).catch(() => void 0);
|
|
607
|
+
for (const name of plan.folders) {
|
|
608
|
+
const dir = path2.join(deskRoot, name);
|
|
609
|
+
try {
|
|
610
|
+
if (await inUse(dir)) throw new Error("in use");
|
|
611
|
+
await removeDir(dir);
|
|
612
|
+
result.folders++;
|
|
613
|
+
} catch {
|
|
614
|
+
result.skipped.push(dir);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const doomed = plan.branches.filter((b) => !held.has(b));
|
|
618
|
+
for (let i = 0; i < doomed.length; i += 40) await g(["branch", "-D", ...doomed.slice(i, i + 40)]).catch(() => void 0);
|
|
619
|
+
if (doomed.length) {
|
|
620
|
+
const left = new Set((await g(["for-each-ref", "--format=%(refname)", "refs/heads/swarm", "refs/heads/qa"])).split(/\r?\n/));
|
|
621
|
+
result.branches = doomed.filter((b) => !left.has(`refs/heads/${b}`)).length;
|
|
622
|
+
}
|
|
623
|
+
return result;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
476
626
|
var LAUNCHERS = ["node.exe", "cmd.exe", "bash.exe", "sh.exe", "conhost.exe", "python.exe", "npm.exe", "npx.exe", "bun.exe", "deno.exe"];
|
|
477
627
|
var pathForms = (p) => [.../* @__PURE__ */ new Set([p, p.replaceAll("\\", "/"), p.replaceAll("\\", "\\\\")])];
|
|
478
628
|
function leftoversInDesk(listing, desk, keep) {
|
|
@@ -537,9 +687,46 @@ Write-Output $kill.Count`;
|
|
|
537
687
|
}
|
|
538
688
|
|
|
539
689
|
// server/agentRunner.ts
|
|
690
|
+
import fs4 from "node:fs";
|
|
691
|
+
import path4 from "node:path";
|
|
692
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
693
|
+
|
|
694
|
+
// server/browser.ts
|
|
540
695
|
import fs3 from "node:fs";
|
|
696
|
+
import { createRequire } from "node:module";
|
|
697
|
+
import os2 from "node:os";
|
|
541
698
|
import path3 from "node:path";
|
|
542
|
-
|
|
699
|
+
function playwrightMcpCli() {
|
|
700
|
+
try {
|
|
701
|
+
const pkgFile = createRequire(import.meta.url).resolve("@playwright/mcp/package.json");
|
|
702
|
+
const bin = JSON.parse(fs3.readFileSync(pkgFile, "utf8")).bin?.["playwright-mcp"] ?? "cli.js";
|
|
703
|
+
return path3.join(path3.dirname(pkgFile), bin);
|
|
704
|
+
} catch {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function playwrightMcpCommand(cli, initScript, outputDir, node = process.execPath) {
|
|
709
|
+
return { command: node, args: [cli, "--headless", "--isolated", ...outputDir ? ["--output-dir", outputDir] : [], "--init-script", initScript] };
|
|
710
|
+
}
|
|
711
|
+
function playwrightMcp(outputDir) {
|
|
712
|
+
const cli = playwrightMcpCli();
|
|
713
|
+
return cli ? playwrightMcpCommand(cli, path3.join(import.meta.dirname, "browser-init.js"), outputDir) : null;
|
|
714
|
+
}
|
|
715
|
+
function chromePaths(platform = process.platform, env = process.env, home = os2.homedir()) {
|
|
716
|
+
if (platform === "win32") {
|
|
717
|
+
return [env.LOCALAPPDATA, env.PROGRAMFILES, env["PROGRAMFILES(X86)"]].filter((d) => !!d).map((d) => path3.win32.join(d, "Google", "Chrome", "Application", "chrome.exe"));
|
|
718
|
+
}
|
|
719
|
+
if (platform === "darwin") return ["/Applications/Google Chrome.app", path3.posix.join(home, "Applications", "Google Chrome.app")];
|
|
720
|
+
return ["/opt/google/chrome/chrome", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"];
|
|
721
|
+
}
|
|
722
|
+
var hasChrome = () => chromePaths().some((p) => fs3.existsSync(p));
|
|
723
|
+
function browserProblem(mcp, chrome = hasChrome()) {
|
|
724
|
+
if (!mcp) return "\u26A0 The browser tool (@playwright/mcp) is not installed with cubefarm: reinstall it, or run npx cubefarm doctor";
|
|
725
|
+
if (!chrome) return "\u26A0 Google Chrome not found, so this session cannot test in a browser: install it, or run npx cubefarm doctor";
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// server/agentRunner.ts
|
|
543
730
|
var InputQueue = class {
|
|
544
731
|
items = [];
|
|
545
732
|
waiters = [];
|
|
@@ -571,7 +758,7 @@ function screenshotFile(cwd, text) {
|
|
|
571
758
|
const file = text.match(/\]\(([^)\s]+\.(png|jpe?g))\)/i);
|
|
572
759
|
if (!file) return null;
|
|
573
760
|
try {
|
|
574
|
-
return { data:
|
|
761
|
+
return { data: fs4.readFileSync(path4.resolve(cwd, file[1])), mime: /png$/i.test(file[2]) ? "image/png" : "image/jpeg" };
|
|
575
762
|
} catch {
|
|
576
763
|
return null;
|
|
577
764
|
}
|
|
@@ -580,17 +767,17 @@ var IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jp
|
|
|
580
767
|
function newScreenshots(dir, seen, now2 = Date.now()) {
|
|
581
768
|
let names;
|
|
582
769
|
try {
|
|
583
|
-
names =
|
|
770
|
+
names = fs4.readdirSync(dir);
|
|
584
771
|
} catch {
|
|
585
772
|
return [];
|
|
586
773
|
}
|
|
587
774
|
const found2 = [];
|
|
588
775
|
for (const name of names.sort()) {
|
|
589
|
-
const mime = IMAGE_MIME[
|
|
590
|
-
const file =
|
|
776
|
+
const mime = IMAGE_MIME[path4.extname(name).toLowerCase()];
|
|
777
|
+
const file = path4.join(dir, name);
|
|
591
778
|
if (!mime || seen.has(name)) continue;
|
|
592
779
|
try {
|
|
593
|
-
if (now2 -
|
|
780
|
+
if (now2 - fs4.statSync(file).mtimeMs < 1e3) continue;
|
|
594
781
|
} catch {
|
|
595
782
|
continue;
|
|
596
783
|
}
|
|
@@ -602,7 +789,7 @@ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
|
602
789
|
function rel(cwd, p) {
|
|
603
790
|
const s = String(p ?? "");
|
|
604
791
|
if (!s) return "";
|
|
605
|
-
const r =
|
|
792
|
+
const r = path4.relative(cwd, path4.resolve(cwd, s));
|
|
606
793
|
return r && !r.startsWith("..") ? r.replaceAll("\\", "/") : s;
|
|
607
794
|
}
|
|
608
795
|
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -729,8 +916,10 @@ function startSession(opts, callbacks) {
|
|
|
729
916
|
env.CLAUDE_AGENT_SDK_CLIENT_APP = `cubefarm/${VERSION}`;
|
|
730
917
|
const mcpServers = {};
|
|
731
918
|
if (opts.browserTesting) {
|
|
732
|
-
const
|
|
733
|
-
mcpServers.playwright =
|
|
919
|
+
const browser = playwrightMcp();
|
|
920
|
+
if (browser) mcpServers.playwright = browser;
|
|
921
|
+
const problem = browserProblem(browser);
|
|
922
|
+
if (problem) cb.log([{ kind: "error", text: problem }]);
|
|
734
923
|
}
|
|
735
924
|
if (opts.office) mcpServers.office = opts.office.server;
|
|
736
925
|
const disallowedTools = ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode"];
|
|
@@ -903,15 +1092,15 @@ function startSession(opts, callbacks) {
|
|
|
903
1092
|
|
|
904
1093
|
// server/cliRunner.ts
|
|
905
1094
|
import crypto3 from "node:crypto";
|
|
906
|
-
import
|
|
907
|
-
import
|
|
1095
|
+
import fs7 from "node:fs";
|
|
1096
|
+
import path8 from "node:path";
|
|
908
1097
|
import { pathToFileURL } from "node:url";
|
|
909
1098
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
910
1099
|
|
|
911
1100
|
// server/clis.ts
|
|
912
|
-
import
|
|
913
|
-
import { createRequire } from "node:module";
|
|
914
|
-
import
|
|
1101
|
+
import fs5 from "node:fs";
|
|
1102
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
1103
|
+
import path5 from "node:path";
|
|
915
1104
|
var CLIS = [
|
|
916
1105
|
{ id: "claude", label: "Claude Code", command: "claude", integrated: true },
|
|
917
1106
|
{ id: "codex", label: "Codex", command: "codex", integrated: false },
|
|
@@ -922,13 +1111,13 @@ var cliLabel = (id) => CLIS.find((c) => c.id === id)?.label ?? id;
|
|
|
922
1111
|
var WIN = process.platform === "win32";
|
|
923
1112
|
function resolveCommand(name, env = process.env) {
|
|
924
1113
|
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
|
|
925
|
-
const dirs = (env[key] ?? "").split(
|
|
1114
|
+
const dirs = (env[key] ?? "").split(path5.delimiter).filter(Boolean);
|
|
926
1115
|
const exts = WIN ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((e) => /^\.(exe|cmd|bat|com)$/i.test(e)) : [""];
|
|
927
1116
|
for (const dir of dirs) {
|
|
928
1117
|
for (const ext of exts) {
|
|
929
|
-
const file =
|
|
1118
|
+
const file = path5.join(dir, name + ext.toLowerCase());
|
|
930
1119
|
try {
|
|
931
|
-
const st =
|
|
1120
|
+
const st = fs5.statSync(file);
|
|
932
1121
|
if (st.isFile() && (WIN || (st.mode & 73) !== 0)) return file;
|
|
933
1122
|
} catch {
|
|
934
1123
|
}
|
|
@@ -939,16 +1128,16 @@ function resolveCommand(name, env = process.env) {
|
|
|
939
1128
|
function unwrapCmdShim(shimPath, text) {
|
|
940
1129
|
const line = text.split(/\r?\n/).find((l) => /%\*\s*$/.test(l));
|
|
941
1130
|
if (!line) return null;
|
|
942
|
-
const dir =
|
|
943
|
-
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) =>
|
|
1131
|
+
const dir = path5.dirname(shimPath);
|
|
1132
|
+
const targets = [...line.matchAll(/"%dp0%\\([^"]+)"/g)].map((m) => path5.join(dir, m[1]));
|
|
944
1133
|
const target = targets[targets.length - 1];
|
|
945
1134
|
if (!target) return null;
|
|
946
1135
|
if (/\.exe$/i.test(target)) return { file: target, args: [] };
|
|
947
1136
|
if (!/\.[cm]?js$/i.test(target)) return null;
|
|
948
1137
|
const between = line.slice(line.indexOf('"%_prog%"') + '"%_prog%"'.length, line.lastIndexOf(`"%dp0%\\`));
|
|
949
1138
|
const nodeFlags = line.includes('"%_prog%"') ? between.trim().split(/\s+/).filter((f) => f.startsWith("--")) : [];
|
|
950
|
-
const localNode =
|
|
951
|
-
return { file:
|
|
1139
|
+
const localNode = path5.join(dir, "node.exe");
|
|
1140
|
+
return { file: fs5.existsSync(localNode) ? localNode : process.execPath, args: [...nodeFlags, target] };
|
|
952
1141
|
}
|
|
953
1142
|
var bundled;
|
|
954
1143
|
function bundledClaude() {
|
|
@@ -956,7 +1145,7 @@ function bundledClaude() {
|
|
|
956
1145
|
bundled = null;
|
|
957
1146
|
let sdk;
|
|
958
1147
|
try {
|
|
959
|
-
sdk =
|
|
1148
|
+
sdk = createRequire2(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk");
|
|
960
1149
|
} catch {
|
|
961
1150
|
return null;
|
|
962
1151
|
}
|
|
@@ -966,7 +1155,7 @@ function bundledClaude() {
|
|
|
966
1155
|
const targets = platform === "linux" ? musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
967
1156
|
for (const target of targets) {
|
|
968
1157
|
try {
|
|
969
|
-
bundled =
|
|
1158
|
+
bundled = createRequire2(sdk).resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${WIN ? ".exe" : ""}`);
|
|
970
1159
|
return bundled;
|
|
971
1160
|
} catch {
|
|
972
1161
|
}
|
|
@@ -981,7 +1170,7 @@ function commandFor(id) {
|
|
|
981
1170
|
if (!found2) return null;
|
|
982
1171
|
if (WIN && /\.(cmd|bat)$/i.test(found2)) {
|
|
983
1172
|
try {
|
|
984
|
-
return unwrapCmdShim(found2,
|
|
1173
|
+
return unwrapCmdShim(found2, fs5.readFileSync(found2, "utf8")) ?? { file: found2, args: [] };
|
|
985
1174
|
} catch {
|
|
986
1175
|
return { file: found2, args: [] };
|
|
987
1176
|
}
|
|
@@ -1183,18 +1372,18 @@ export default async function CubefarmPlugin({ client } = {}) {
|
|
|
1183
1372
|
|
|
1184
1373
|
// server/ptyClient.ts
|
|
1185
1374
|
import crypto2 from "node:crypto";
|
|
1186
|
-
import
|
|
1375
|
+
import fs6 from "node:fs";
|
|
1187
1376
|
import net from "node:net";
|
|
1188
|
-
import
|
|
1377
|
+
import path7 from "node:path";
|
|
1189
1378
|
import { spawn } from "node:child_process";
|
|
1190
1379
|
|
|
1191
1380
|
// server/ptyProtocol.ts
|
|
1192
1381
|
import crypto from "node:crypto";
|
|
1193
|
-
import
|
|
1382
|
+
import path6 from "node:path";
|
|
1194
1383
|
var PTY_PROTOCOL = 1;
|
|
1195
1384
|
function hostSocket(home, platform = process.platform) {
|
|
1196
|
-
if (platform !== "win32") return
|
|
1197
|
-
const tag = crypto.createHash("sha1").update(
|
|
1385
|
+
if (platform !== "win32") return path6.join(home, "pty.sock");
|
|
1386
|
+
const tag = crypto.createHash("sha1").update(path6.resolve(home).toLowerCase()).digest("hex").slice(0, 12);
|
|
1198
1387
|
return `\\\\.\\pipe\\cubefarm-pty-${tag}`;
|
|
1199
1388
|
}
|
|
1200
1389
|
var encode = (m) => `${JSON.stringify(m)}
|
|
@@ -1325,7 +1514,7 @@ function connect(socket, secret) {
|
|
|
1325
1514
|
}
|
|
1326
1515
|
var runtimeFlags = () => process.execArgv.filter((a, i, all) => !/^--(inspect|watch|debug)/.test(a) && !/^--(inspect|watch|debug)/.test(all[i - 1] ?? ""));
|
|
1327
1516
|
function launch(socket, secretFile) {
|
|
1328
|
-
const entry =
|
|
1517
|
+
const entry = path7.join(import.meta.dirname, `ptyHost${path7.extname(import.meta.filename)}`);
|
|
1329
1518
|
spawn(process.execPath, [...runtimeFlags(), entry, "launch", socket, secretFile], { detached: true, stdio: "ignore", windowsHide: true }).once("error", () => void 0).unref();
|
|
1330
1519
|
}
|
|
1331
1520
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -1333,17 +1522,17 @@ async function startKeeper(home, onHook) {
|
|
|
1333
1522
|
hookHandler = onHook;
|
|
1334
1523
|
if (!nodePty) return [];
|
|
1335
1524
|
const socket = hostSocket(home);
|
|
1336
|
-
const secretFile =
|
|
1525
|
+
const secretFile = path7.join(home, "pty.secret");
|
|
1337
1526
|
try {
|
|
1338
|
-
if (!
|
|
1339
|
-
|
|
1340
|
-
|
|
1527
|
+
if (!fs6.existsSync(secretFile)) {
|
|
1528
|
+
fs6.mkdirSync(home, { recursive: true });
|
|
1529
|
+
fs6.writeFileSync(secretFile, crypto2.randomBytes(24).toString("hex"), { mode: 384 });
|
|
1341
1530
|
}
|
|
1342
1531
|
} catch (err) {
|
|
1343
1532
|
console.warn(` terminal keeper unavailable: ${err.message}`);
|
|
1344
1533
|
return [];
|
|
1345
1534
|
}
|
|
1346
|
-
const secret =
|
|
1535
|
+
const secret = fs6.readFileSync(secretFile, "utf8").trim();
|
|
1347
1536
|
let hello = await connect(socket, secret);
|
|
1348
1537
|
if (hello && hello.ready.version !== PTY_PROTOCOL) {
|
|
1349
1538
|
console.log(` replacing the terminal keeper (v${hello.ready.version}); the ${hello.ready.ptys.length} terminal(s) it held are stopped`);
|
|
@@ -1423,8 +1612,8 @@ function killTree(proc) {
|
|
|
1423
1612
|
}
|
|
1424
1613
|
|
|
1425
1614
|
// server/cliRunner.ts
|
|
1426
|
-
var SESSIONS_DIR =
|
|
1427
|
-
var BIN_DIR =
|
|
1615
|
+
var SESSIONS_DIR = path8.join(HOME_DIR, "sessions");
|
|
1616
|
+
var BIN_DIR = path8.join(HOME_DIR, "bin");
|
|
1428
1617
|
var FINISH_GRACE_MS = 3e3;
|
|
1429
1618
|
var BOOT_MS = 6e4;
|
|
1430
1619
|
var USAGE_WARN_PCT = 90;
|
|
@@ -1462,19 +1651,15 @@ async function handleMcp(token, req, res) {
|
|
|
1462
1651
|
var helpersWritten = false;
|
|
1463
1652
|
function writeHelpers() {
|
|
1464
1653
|
if (helpersWritten) return;
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1654
|
+
fs7.mkdirSync(BIN_DIR, { recursive: true });
|
|
1655
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "statusline.cjs"), STATUSLINE_SOURCE);
|
|
1656
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "notify.cjs"), NOTIFY_SOURCE);
|
|
1657
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "codex-hook.cjs"), CODEX_HOOK_SOURCE);
|
|
1658
|
+
fs7.writeFileSync(path8.join(BIN_DIR, "opencode-plugin.mjs"), OPENCODE_PLUGIN_SOURCE);
|
|
1470
1659
|
helpersWritten = true;
|
|
1471
1660
|
}
|
|
1472
1661
|
var shellArg = (p) => `"${p.replaceAll("\\", "/")}"`;
|
|
1473
1662
|
var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop", "StopFailure", "Notification"];
|
|
1474
|
-
function playwrightServer(outputDir) {
|
|
1475
|
-
const args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--output-dir", outputDir, "--init-script", path7.join(import.meta.dirname, "browser-init.js")];
|
|
1476
|
-
return process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
|
|
1477
|
-
}
|
|
1478
1663
|
function toolOutput(response) {
|
|
1479
1664
|
const images = [];
|
|
1480
1665
|
const blocks = Array.isArray(response) ? response : Array.isArray(response?.content) ? response.content : null;
|
|
@@ -1521,7 +1706,7 @@ function endLive(live) {
|
|
|
1521
1706
|
live.proc?.kill();
|
|
1522
1707
|
live.proc = null;
|
|
1523
1708
|
live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
|
|
1524
|
-
|
|
1709
|
+
fs7.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
|
|
1525
1710
|
return live.cli === "codex" && live.resumeId ? codexThread("archive", live.resumeId, gone) : Promise.resolve();
|
|
1526
1711
|
}
|
|
1527
1712
|
var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
|
|
@@ -1605,7 +1790,7 @@ function startCliSession(opts, callbacks) {
|
|
|
1605
1790
|
const label = cliLabel(cli);
|
|
1606
1791
|
const token = crypto3.randomUUID();
|
|
1607
1792
|
const sessionId = opts.resumeSessionId ?? crypto3.randomUUID();
|
|
1608
|
-
const dir =
|
|
1793
|
+
const dir = path8.join(SESSIONS_DIR, token);
|
|
1609
1794
|
const started = Date.now();
|
|
1610
1795
|
const statusLine = `\u{1F3E2} cubefarm${opts.label ? ` \xB7 ${opts.label}` : ""}`;
|
|
1611
1796
|
const toolNames = /* @__PURE__ */ new Map();
|
|
@@ -1633,10 +1818,10 @@ function startCliSession(opts, callbacks) {
|
|
|
1633
1818
|
const collectShots = (all = false) => {
|
|
1634
1819
|
const l2 = live;
|
|
1635
1820
|
if (!l2 || cli === "claude" || !opts.browserTesting) return;
|
|
1636
|
-
for (const shot of newScreenshots(
|
|
1821
|
+
for (const shot of newScreenshots(path8.join(l2.dir, "browser"), l2.shots, all ? Infinity : Date.now())) {
|
|
1637
1822
|
l2.shots.add(shot.name);
|
|
1638
1823
|
try {
|
|
1639
|
-
cb.screenshot(
|
|
1824
|
+
cb.screenshot(fs7.readFileSync(shot.file), shot.mime);
|
|
1640
1825
|
} catch {
|
|
1641
1826
|
}
|
|
1642
1827
|
}
|
|
@@ -1707,7 +1892,7 @@ function startCliSession(opts, callbacks) {
|
|
|
1707
1892
|
const event = String(b.hook_event_name ?? "");
|
|
1708
1893
|
const sub = typeof b.agent_id === "string" && b.agent_id !== "";
|
|
1709
1894
|
if (cli === "claude" && !sub && b.session_id !== kept) {
|
|
1710
|
-
const id = resumableSession(b,
|
|
1895
|
+
const id = resumableSession(b, fs7.existsSync);
|
|
1711
1896
|
if (id) {
|
|
1712
1897
|
kept = id;
|
|
1713
1898
|
cb.sessionId(id);
|
|
@@ -1859,7 +2044,9 @@ function startCliSession(opts, callbacks) {
|
|
|
1859
2044
|
}
|
|
1860
2045
|
const keeper = keeperHookUrl();
|
|
1861
2046
|
const hookUrl = `${keeper ?? officeUrl}/api/hooks/${token}`;
|
|
1862
|
-
const browser = opts.browserTesting ?
|
|
2047
|
+
const browser = opts.browserTesting ? playwrightMcp(path8.join(dir, "browser")) : null;
|
|
2048
|
+
const browserIssue = opts.browserTesting ? browserProblem(browser) : null;
|
|
2049
|
+
if (browserIssue) log([{ kind: "error", text: browserIssue }]);
|
|
1863
2050
|
let prompt = opts.prompt;
|
|
1864
2051
|
if (opts.outputSchema) {
|
|
1865
2052
|
prompt += `
|
|
@@ -1870,21 +2057,21 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1870
2057
|
let files;
|
|
1871
2058
|
try {
|
|
1872
2059
|
writeHelpers();
|
|
1873
|
-
|
|
2060
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1874
2061
|
const settings = {
|
|
1875
2062
|
hooks: Object.fromEntries(HOOK_EVENTS.map((e) => [e, [{ hooks: [{ type: "http", url: hookUrl, timeout: keeper ? 120 : 30 }] }]])),
|
|
1876
|
-
statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(
|
|
2063
|
+
statusLine: { type: "command", command: `${shellArg(process.execPath)} ${shellArg(path8.join(BIN_DIR, "statusline.cjs"))} ${shellArg(hookUrl)}`, padding: 0 }
|
|
1877
2064
|
};
|
|
1878
2065
|
const servers = {};
|
|
1879
2066
|
if (browser) servers.playwright = browser;
|
|
1880
2067
|
if (opts.office) servers.office = { type: "http", url: `${officeUrl}/api/mcp/${token}` };
|
|
1881
|
-
files = { settings:
|
|
1882
|
-
|
|
1883
|
-
if (files.mcp)
|
|
1884
|
-
|
|
2068
|
+
files = { settings: path8.join(dir, "settings.json"), mcp: Object.keys(servers).length ? path8.join(dir, "mcp.json") : null, system: path8.join(dir, "instructions.md") };
|
|
2069
|
+
fs7.writeFileSync(files.settings, JSON.stringify(settings, null, 2));
|
|
2070
|
+
if (files.mcp) fs7.writeFileSync(files.mcp, JSON.stringify({ mcpServers: servers }, null, 2));
|
|
2071
|
+
fs7.writeFileSync(files.system, opts.systemAppend);
|
|
1885
2072
|
if (prompt.length > INLINE_PROMPT_MAX) {
|
|
1886
|
-
const file =
|
|
1887
|
-
|
|
2073
|
+
const file = path8.join(dir, "task.md");
|
|
2074
|
+
fs7.writeFileSync(file, prompt);
|
|
1888
2075
|
prompt = `Your task is in ${file}. Read all of it first, then do what it says.`;
|
|
1889
2076
|
}
|
|
1890
2077
|
} catch (err) {
|
|
@@ -1904,9 +2091,9 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1904
2091
|
role: opts.role,
|
|
1905
2092
|
additionalDirectories: opts.additionalDirectories,
|
|
1906
2093
|
files,
|
|
1907
|
-
notify: { script:
|
|
1908
|
-
codexHook:
|
|
1909
|
-
plugin: pathToFileURL(
|
|
2094
|
+
notify: { script: path8.join(BIN_DIR, "notify.cjs"), url: hookUrl },
|
|
2095
|
+
codexHook: path8.join(BIN_DIR, "codex-hook.cjs"),
|
|
2096
|
+
plugin: pathToFileURL(path8.join(BIN_DIR, "opencode-plugin.mjs")).href,
|
|
1910
2097
|
browser
|
|
1911
2098
|
});
|
|
1912
2099
|
const env = {};
|
|
@@ -1972,9 +2159,9 @@ ${JSON.stringify(opts.outputSchema)}`;
|
|
|
1972
2159
|
// server/previewRunner.ts
|
|
1973
2160
|
import { spawn as spawn2 } from "node:child_process";
|
|
1974
2161
|
import crypto4 from "node:crypto";
|
|
1975
|
-
import
|
|
2162
|
+
import fs8 from "node:fs/promises";
|
|
1976
2163
|
import net2 from "node:net";
|
|
1977
|
-
import
|
|
2164
|
+
import path9 from "node:path";
|
|
1978
2165
|
var PREVIEW_SLUG = "preview";
|
|
1979
2166
|
var PREVIEW_BRANCH = "swarm-preview";
|
|
1980
2167
|
var PREVIEW_TMP = ".preview-tmp";
|
|
@@ -2010,7 +2197,7 @@ async function portOpen(port) {
|
|
|
2010
2197
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2011
2198
|
async function readJson(file) {
|
|
2012
2199
|
try {
|
|
2013
|
-
return JSON.parse(await
|
|
2200
|
+
return JSON.parse(await fs8.readFile(file, "utf8"));
|
|
2014
2201
|
} catch {
|
|
2015
2202
|
return null;
|
|
2016
2203
|
}
|
|
@@ -2071,9 +2258,9 @@ var RealPreview = class {
|
|
|
2071
2258
|
this.check();
|
|
2072
2259
|
await ensureLocalExcludes(job.fullName).catch(() => void 0);
|
|
2073
2260
|
cb.commit(await git(["rev-parse", "--short", "HEAD"], { cwd: wt }).catch(() => ""));
|
|
2074
|
-
const tmp =
|
|
2075
|
-
await
|
|
2076
|
-
const pkg = await readJson(
|
|
2261
|
+
const tmp = path9.join(wt, PREVIEW_TMP);
|
|
2262
|
+
await fs8.mkdir(tmp, { recursive: true });
|
|
2263
|
+
const pkg = await readJson(path9.join(wt, "package.json"));
|
|
2077
2264
|
const command = job.command ?? defaultCommand(pkg);
|
|
2078
2265
|
if (!command) {
|
|
2079
2266
|
this.stopped = true;
|
|
@@ -2112,11 +2299,11 @@ var RealPreview = class {
|
|
|
2112
2299
|
}
|
|
2113
2300
|
/** npm ci (or npm install without a lockfile), skipped when package.json and the lockfile haven't changed. */
|
|
2114
2301
|
async install(wt, env) {
|
|
2115
|
-
const lockFile =
|
|
2116
|
-
const lock = await
|
|
2117
|
-
const hash = crypto4.createHash("sha256").update(await
|
|
2118
|
-
const marker =
|
|
2119
|
-
if (await
|
|
2302
|
+
const lockFile = path9.join(wt, "package-lock.json");
|
|
2303
|
+
const lock = await fs8.readFile(lockFile, "utf8").catch(() => null);
|
|
2304
|
+
const hash = crypto4.createHash("sha256").update(await fs8.readFile(path9.join(wt, "package.json"), "utf8")).update("\0").update(lock ?? "").digest("hex");
|
|
2305
|
+
const marker = path9.join(wt, "node_modules", INSTALL_MARKER);
|
|
2306
|
+
if (await fs8.readFile(marker, "utf8").catch(() => "") === hash) {
|
|
2120
2307
|
this.cb.log(["Dependencies unchanged since the last install; skipping it."]);
|
|
2121
2308
|
return;
|
|
2122
2309
|
}
|
|
@@ -2130,7 +2317,7 @@ var RealPreview = class {
|
|
|
2130
2317
|
}
|
|
2131
2318
|
if (code !== 0) throw new Error(`${cmd} failed (code ${code}).`);
|
|
2132
2319
|
this.child = null;
|
|
2133
|
-
await
|
|
2320
|
+
await fs8.writeFile(marker, hash).catch(() => void 0);
|
|
2134
2321
|
}
|
|
2135
2322
|
/** Run a command line through the platform shell; resolves with its exit code. */
|
|
2136
2323
|
spawn(cmd, cwd, env) {
|
|
@@ -2161,12 +2348,12 @@ var RealPreview = class {
|
|
|
2161
2348
|
};
|
|
2162
2349
|
var realPreviews = {
|
|
2163
2350
|
start: (job, cb) => new RealPreview(job, cb),
|
|
2164
|
-
hasDefault: async (fullName) =>
|
|
2351
|
+
hasDefault: async (fullName) => fs8.access(path9.join(mainDir(fullName), "package.json")).then(() => true).catch(() => false)
|
|
2165
2352
|
};
|
|
2166
2353
|
|
|
2167
2354
|
// server/officeUpdate.ts
|
|
2168
|
-
import
|
|
2169
|
-
import
|
|
2355
|
+
import fs9 from "node:fs/promises";
|
|
2356
|
+
import path10 from "node:path";
|
|
2170
2357
|
var DRAIN_TIMEOUT_MS = 20 * 6e4;
|
|
2171
2358
|
var POSTPONE_MS = 2 * 60 * 6e4;
|
|
2172
2359
|
function underLauncher(proc = process) {
|
|
@@ -2203,10 +2390,10 @@ function parseLastUpdate(raw) {
|
|
|
2203
2390
|
}
|
|
2204
2391
|
}
|
|
2205
2392
|
async function takeLastUpdate(home) {
|
|
2206
|
-
const file =
|
|
2207
|
-
const raw = await
|
|
2393
|
+
const file = path10.join(home, "last-update.json");
|
|
2394
|
+
const raw = await fs9.readFile(file, "utf8").catch(() => null);
|
|
2208
2395
|
if (raw === null) return null;
|
|
2209
|
-
await
|
|
2396
|
+
await fs9.rm(file, { force: true, maxRetries: 3 }).catch(() => void 0);
|
|
2210
2397
|
return parseLastUpdate(raw);
|
|
2211
2398
|
}
|
|
2212
2399
|
function lastUpdateMessage(u, commits) {
|
|
@@ -2214,8 +2401,8 @@ function lastUpdateMessage(u, commits) {
|
|
|
2214
2401
|
const n = commits === null ? "" : ` (${commits} commit${commits === 1 ? "" : "s"})`;
|
|
2215
2402
|
return `\u2B06\uFE0F Updated the office from ${u.from.slice(0, 7)} to ${u.to.slice(0, 7)}${n}`;
|
|
2216
2403
|
}
|
|
2217
|
-
var OFFICE_DIR =
|
|
2218
|
-
var isSamePath = (a, b) => process.platform === "win32" ?
|
|
2404
|
+
var OFFICE_DIR = path10.resolve(import.meta.dirname, "..");
|
|
2405
|
+
var isSamePath = (a, b) => process.platform === "win32" ? path10.resolve(a).toLowerCase() === path10.resolve(b).toLowerCase() : path10.resolve(a) === path10.resolve(b);
|
|
2219
2406
|
var realOffice = {
|
|
2220
2407
|
launcher: underLauncher(),
|
|
2221
2408
|
head: () => git(["rev-parse", "HEAD"], { cwd: OFFICE_DIR }).catch(() => null),
|
|
@@ -2240,6 +2427,7 @@ var realBackend = {
|
|
|
2240
2427
|
createIssue,
|
|
2241
2428
|
issueState,
|
|
2242
2429
|
editIssue,
|
|
2430
|
+
closeIssue,
|
|
2243
2431
|
mergePull,
|
|
2244
2432
|
updateBranch,
|
|
2245
2433
|
closePull,
|
|
@@ -2259,6 +2447,7 @@ var realBackend = {
|
|
|
2259
2447
|
deskDir,
|
|
2260
2448
|
prepareDesk,
|
|
2261
2449
|
removeDesk,
|
|
2450
|
+
sweepDesks,
|
|
2262
2451
|
releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
|
|
2263
2452
|
startSession: (opts, cb) => opts.terminal ? startCliSession(opts, cb) : startSession(opts, cb),
|
|
2264
2453
|
terminals: terminalsAvailable,
|
|
@@ -2273,7 +2462,7 @@ var realBackend = {
|
|
|
2273
2462
|
// server/demo.ts
|
|
2274
2463
|
import crypto5 from "node:crypto";
|
|
2275
2464
|
import http from "node:http";
|
|
2276
|
-
import
|
|
2465
|
+
import path11 from "node:path";
|
|
2277
2466
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
2278
2467
|
function issue(n, title, body, fullName, labels = []) {
|
|
2279
2468
|
return { number: n, title, body, url: `https://github.com/${fullName}/issues/${n}`, labels, createdAt: now() };
|
|
@@ -2326,7 +2515,7 @@ function screenshotSvg(title, url, hue) {
|
|
|
2326
2515
|
</svg>`;
|
|
2327
2516
|
}
|
|
2328
2517
|
function devScript(opts, cb, issueNumber, issueTitle) {
|
|
2329
|
-
const branch =
|
|
2518
|
+
const branch = path11.basename(opts.cwd);
|
|
2330
2519
|
const hue = issueNumber * 67 % 360;
|
|
2331
2520
|
const file = ["src/App.tsx", "src/components/TodoList.tsx", "src/api/routes.ts", "src/styles.css"][issueNumber % 4];
|
|
2332
2521
|
const port = 5200 + issueNumber % 50;
|
|
@@ -2465,7 +2654,7 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2465
2654
|
number: n,
|
|
2466
2655
|
title,
|
|
2467
2656
|
url,
|
|
2468
|
-
headRefName: `swarm/issue-${number}-${
|
|
2657
|
+
headRefName: `swarm/issue-${number}-${path11.basename(opts.cwd).replace(/-[0-9a-f]{4}$/, "")}`,
|
|
2469
2658
|
state: "OPEN",
|
|
2470
2659
|
isDraft: false,
|
|
2471
2660
|
mergeable: "MERGEABLE",
|
|
@@ -2660,8 +2849,16 @@ function createDemoBackend() {
|
|
|
2660
2849
|
mergedSinceSync.set(fullName, (mergedSinceSync.get(fullName) ?? 0) + 1);
|
|
2661
2850
|
pr.state = "MERGED";
|
|
2662
2851
|
pr.mergedAt = now();
|
|
2663
|
-
|
|
2664
|
-
|
|
2852
|
+
setTimeout(() => {
|
|
2853
|
+
for (const n of pr.closesIssues) closedIssues.add(`${fullName}#${n}`);
|
|
2854
|
+
r.issues = r.issues.filter((i) => !pr.closesIssues.includes(i.number));
|
|
2855
|
+
}, 2e4).unref();
|
|
2856
|
+
},
|
|
2857
|
+
closeIssue: async (fullName, number) => {
|
|
2858
|
+
const r = repos.get(fullName);
|
|
2859
|
+
if (!r?.issues.some((i) => i.number === number)) throw new Error(`Issue #${number} is not open`);
|
|
2860
|
+
closedIssues.add(`${fullName}#${number}`);
|
|
2861
|
+
r.issues = r.issues.filter((i) => i.number !== number);
|
|
2665
2862
|
},
|
|
2666
2863
|
updateBranch: async (fullName, number) => {
|
|
2667
2864
|
const pr = repos.get(fullName)?.pulls.find((p) => p.number === number);
|
|
@@ -2716,6 +2913,7 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2716
2913
|
return dir;
|
|
2717
2914
|
},
|
|
2718
2915
|
removeDesk: async () => void 0,
|
|
2916
|
+
sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
|
|
2719
2917
|
releaseDesk: async () => void 0,
|
|
2720
2918
|
startSession: (opts, cb) => {
|
|
2721
2919
|
if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
|
|
@@ -2912,7 +3110,8 @@ function ceoSession(opts, cb) {
|
|
|
2912
3110
|
const s = JSON.parse(await office.call("company_status", {}));
|
|
2913
3111
|
const people = s.floors.reduce((n, f) => n + f.team.length, 0);
|
|
2914
3112
|
const issues = s.floors.reduce((n, f) => n + f.backlog.length, 0);
|
|
2915
|
-
|
|
3113
|
+
const free = s.floors.reduce((n, f) => n + f.seats.dev.free + f.seats.qa.free, 0);
|
|
3114
|
+
cb.log([{ kind: "result", text: ` \u23BF ${s.floors.length} floors \xB7 ${people} people \xB7 ${free} free seats \xB7 ${issues} open issues \xB7 ${s.pendingProposals.length} proposals pending` }]);
|
|
2916
3115
|
return s;
|
|
2917
3116
|
};
|
|
2918
3117
|
const use = async (name, args) => {
|
|
@@ -3103,8 +3302,8 @@ Acceptance criteria:
|
|
|
3103
3302
|
}
|
|
3104
3303
|
|
|
3105
3304
|
// server/swarm.ts
|
|
3106
|
-
import
|
|
3107
|
-
import
|
|
3305
|
+
import fs11 from "node:fs/promises";
|
|
3306
|
+
import path13 from "node:path";
|
|
3108
3307
|
import crypto6 from "node:crypto";
|
|
3109
3308
|
|
|
3110
3309
|
// server/ceo.ts
|
|
@@ -3189,7 +3388,7 @@ function createOfficeTools(h) {
|
|
|
3189
3388
|
const defs = [
|
|
3190
3389
|
tool(
|
|
3191
3390
|
"company_status",
|
|
3192
|
-
"Everything about the company right now: settings, every floor (repo, clone path, brief, profile, QA brief), its team, backlog, pull requests and QA, pending proposals and recent decisions by the manager. Call this first.",
|
|
3391
|
+
"Everything about the company right now: settings, every floor (repo, clone path, brief, profile, QA brief), its free seats, team, backlog, pull requests and QA, pending proposals and recent decisions by the manager. Call this first.",
|
|
3193
3392
|
{},
|
|
3194
3393
|
() => run2(() => h.companyStatus())
|
|
3195
3394
|
),
|
|
@@ -3281,6 +3480,12 @@ function createOfficeTools(h) {
|
|
|
3281
3480
|
}
|
|
3282
3481
|
};
|
|
3283
3482
|
}
|
|
3483
|
+
var FLOOR_DESKS = { dev: 12, qa: 3 };
|
|
3484
|
+
var MAX_PENDING_PROPOSALS = FLOOR_DESKS.dev + FLOOR_DESKS.qa;
|
|
3485
|
+
var seatCount = (total, taken, proposed) => ({ total, taken, proposed, free: Math.max(0, total - taken - proposed) });
|
|
3486
|
+
function checkPendingLimit(pending2, max = MAX_PENDING_PROPOSALS) {
|
|
3487
|
+
if (pending2 >= max) throw new Error(`${pending2} proposals are already waiting for the manager; propose the rest after they decide.`);
|
|
3488
|
+
}
|
|
3284
3489
|
function ceoSystemPrompt(o) {
|
|
3285
3490
|
const manager = o.manager ? `the manager, ${o.manager}` : "the human manager";
|
|
3286
3491
|
return [
|
|
@@ -3289,7 +3494,7 @@ function ceoSystemPrompt(o) {
|
|
|
3289
3494
|
"",
|
|
3290
3495
|
"Your job is to run the company, not to write code:",
|
|
3291
3496
|
"- Understand each project: what it is, its stack, how far along it is, and what kind of people it needs. Projects differ a lot. A static marketing site, a 3D browser game and a REST API need different specialists and different QA.",
|
|
3292
|
-
`- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: agents on the same coding agent share one subscription's usage limits${o.sessionLimit ? ` and at most ${o.sessionLimit} sessions run at once` : ""}, so a floor rarely needs more than ${o.teamCap} people. Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
|
|
3497
|
+
`- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: agents on the same coding agent share one subscription's usage limits${o.sessionLimit ? ` and at most ${o.sessionLimit} sessions run at once` : ""}, so a floor rarely needs more than ${o.teamCap} people. When the manager asks for a bigger team, follow that, up to the floor's free seats (seats in company_status). Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
|
|
3293
3498
|
"- Plan the work: turn a floor's brief into small, well-specified GitHub issues, one agent-session each, with acceptance criteria. Route each to a specialty. The office hands issues out itself: a free specialist gets first pick of their specialty, and otherwise any free developer takes the next issue that can start, so a specialty is a preference, not a lock.",
|
|
3294
3499
|
"- Write each floor's QA brief: what QA testers must check for this kind of project (for a 3D game: the canvas renders, controls respond, frame rate is smooth; for a website: links, phone layout, accessibility; for an API: status codes, validation, error cases).",
|
|
3295
3500
|
"",
|
|
@@ -3473,6 +3678,25 @@ var HttpError = class extends Error {
|
|
|
3473
3678
|
status;
|
|
3474
3679
|
};
|
|
3475
3680
|
|
|
3681
|
+
// server/issueClaims.ts
|
|
3682
|
+
var RECENT_MERGE_MS = 6 * 60 * 6e4;
|
|
3683
|
+
function branchIssue(headRefName) {
|
|
3684
|
+
const m = /^swarm\/issue-(\d+)-/.exec(headRefName);
|
|
3685
|
+
return m ? Number(m[1]) : null;
|
|
3686
|
+
}
|
|
3687
|
+
function issuesResolvedBy(pr) {
|
|
3688
|
+
const n = branchIssue(pr.headRefName);
|
|
3689
|
+
return [...new Set(n == null ? pr.closesIssues : [...pr.closesIssues, n])];
|
|
3690
|
+
}
|
|
3691
|
+
function issueTaken(n, agents, pulls, now2) {
|
|
3692
|
+
if (agents.some((a) => a.role === "dev" && a.task !== "qa" && a.issueNumber === n && a.status !== "idle" && a.status !== "error")) return true;
|
|
3693
|
+
return pulls.some((p) => {
|
|
3694
|
+
if (p.state === "CLOSED") return false;
|
|
3695
|
+
if (p.state === "MERGED" && (!p.mergedAt || now2 - Date.parse(p.mergedAt) > RECENT_MERGE_MS)) return false;
|
|
3696
|
+
return issuesResolvedBy(p).includes(n);
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3476
3700
|
// server/mergeGate.ts
|
|
3477
3701
|
var MAX_MERGE_FIXES = 3;
|
|
3478
3702
|
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
@@ -3680,6 +3904,7 @@ var Previews = class {
|
|
|
3680
3904
|
}
|
|
3681
3905
|
/** The floor is leaving the building: stop its preview and remove the preview worktree. */
|
|
3682
3906
|
async remove(f) {
|
|
3907
|
+
const main = this.backend.mainDir(f.fullName);
|
|
3683
3908
|
const r = this.runs.get(f.id);
|
|
3684
3909
|
if (r) {
|
|
3685
3910
|
r.gen++;
|
|
@@ -3688,7 +3913,7 @@ var Previews = class {
|
|
|
3688
3913
|
this.runs.delete(f.id);
|
|
3689
3914
|
this.hasDefault.delete(f.id);
|
|
3690
3915
|
clearTimeout(this.emitTimers.get(f.id));
|
|
3691
|
-
await this.backend.removeDesk(f.fullName, PREVIEW_SLUG).catch(() => void 0);
|
|
3916
|
+
await this.backend.removeDesk(f.fullName, PREVIEW_SLUG, main).catch(() => void 0);
|
|
3692
3917
|
}
|
|
3693
3918
|
/** Server shutdown: stop every running preview. */
|
|
3694
3919
|
async stopAll(floors) {
|
|
@@ -3733,6 +3958,95 @@ var Previews = class {
|
|
|
3733
3958
|
}
|
|
3734
3959
|
};
|
|
3735
3960
|
|
|
3961
|
+
// server/prompts.ts
|
|
3962
|
+
var PLACEHOLDERS = {
|
|
3963
|
+
worktree: "<your worktree>",
|
|
3964
|
+
issue: "<issue>",
|
|
3965
|
+
pr: "<PR>",
|
|
3966
|
+
prTitle: "<PR title>",
|
|
3967
|
+
prUrl: "<PR URL>",
|
|
3968
|
+
branch: "<branch>"
|
|
3969
|
+
};
|
|
3970
|
+
var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
|
|
3971
|
+
var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
|
|
3972
|
+
function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing }) {
|
|
3973
|
+
const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
|
|
3974
|
+
const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
|
|
3975
|
+
return [
|
|
3976
|
+
`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.`,
|
|
3977
|
+
`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.`,
|
|
3978
|
+
a.brief ? `
|
|
3979
|
+
Your job description:
|
|
3980
|
+
${a.brief}` : "",
|
|
3981
|
+
"",
|
|
3982
|
+
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
3983
|
+
repo.summary ? `Project: ${repo.summary}` : "",
|
|
3984
|
+
repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
|
|
3985
|
+
`Your worktree: ${cwd}`,
|
|
3986
|
+
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})`,
|
|
3987
|
+
links.length ? `Related repositories you may read for context (do not modify them):
|
|
3988
|
+
${links.join("\n")}` : "",
|
|
3989
|
+
"",
|
|
3990
|
+
"Workflow:",
|
|
3991
|
+
"1. Read the issue and explore the relevant code before changing anything.",
|
|
3992
|
+
"2. Implement the change with focused commits and clear messages.",
|
|
3993
|
+
"3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed.",
|
|
3994
|
+
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.",
|
|
3995
|
+
`5. Push: ${push}`,
|
|
3996
|
+
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>".`,
|
|
3997
|
+
fixing ? "" : "7. End your final message with the pull request URL on its own line.",
|
|
3998
|
+
"",
|
|
3999
|
+
"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."
|
|
4000
|
+
].filter((l) => l !== "").join("\n");
|
|
4001
|
+
}
|
|
4002
|
+
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr }) {
|
|
4003
|
+
return [
|
|
4004
|
+
`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.`,
|
|
4005
|
+
...a.brief ? ["", `Your job description:
|
|
4006
|
+
${a.brief}`] : [],
|
|
4007
|
+
...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."] : [],
|
|
4008
|
+
"",
|
|
4009
|
+
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
4010
|
+
...repo.summary ? [`Project: ${repo.summary}`] : [],
|
|
4011
|
+
...repo.mission ? [`What the team is building (the manager's brief): ${repo.mission}`] : [],
|
|
4012
|
+
...repo.qaBrief ? [`What to check on this project (from the CEO):
|
|
4013
|
+
${repo.qaBrief}`] : [],
|
|
4014
|
+
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
4015
|
+
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
4016
|
+
"",
|
|
4017
|
+
"How to test:",
|
|
4018
|
+
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
4019
|
+
`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.`,
|
|
4020
|
+
"3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4021
|
+
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).",
|
|
4022
|
+
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
4023
|
+
"",
|
|
4024
|
+
"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."
|
|
4025
|
+
].join("\n");
|
|
4026
|
+
}
|
|
4027
|
+
var OFFICE = "Office instructions";
|
|
4028
|
+
var MARK = "\0job description\0";
|
|
4029
|
+
function promptParts(build, brief) {
|
|
4030
|
+
if (!brief) return [{ label: OFFICE, text: build(""), editable: false }];
|
|
4031
|
+
const [before, after] = build(MARK).split(MARK);
|
|
4032
|
+
return [
|
|
4033
|
+
{ label: OFFICE, text: before, editable: false },
|
|
4034
|
+
{ label: "Job description", text: brief, editable: true },
|
|
4035
|
+
{ label: OFFICE, text: after, editable: false }
|
|
4036
|
+
].filter((p) => p.text !== "");
|
|
4037
|
+
}
|
|
4038
|
+
var view = (kind, parts) => ({ kind, text: parts.map((p) => p.text).join(""), parts });
|
|
4039
|
+
function devPromptPreview({ slug, ...i }) {
|
|
4040
|
+
const branch = devBranch(PLACEHOLDERS.issue, slug);
|
|
4041
|
+
return view("dev", promptParts((brief) => devSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch }), i.agent.brief));
|
|
4042
|
+
}
|
|
4043
|
+
function qaPromptPreview({ slug, ...i }) {
|
|
4044
|
+
const pr = { number: PLACEHOLDERS.pr, title: PLACEHOLDERS.prTitle, headRefName: PLACEHOLDERS.branch, url: PLACEHOLDERS.prUrl };
|
|
4045
|
+
const branch = qaBranch(PLACEHOLDERS.pr, slug);
|
|
4046
|
+
return view("qa", promptParts((brief) => qaSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch, pr }), i.agent.brief));
|
|
4047
|
+
}
|
|
4048
|
+
var ceoPromptPreview = (text) => view("ceo", [{ label: OFFICE, text, editable: false }]);
|
|
4049
|
+
|
|
3736
4050
|
// server/pacing.ts
|
|
3737
4051
|
var PACING_MS = 60 * 6e4;
|
|
3738
4052
|
var DEFAULT_PACING_SESSIONS = 3;
|
|
@@ -3771,8 +4085,8 @@ function pacingMessage(w, until, sessions, now2) {
|
|
|
3771
4085
|
}
|
|
3772
4086
|
|
|
3773
4087
|
// server/terminal.ts
|
|
3774
|
-
import
|
|
3775
|
-
import
|
|
4088
|
+
import fs10 from "node:fs/promises";
|
|
4089
|
+
import path12 from "node:path";
|
|
3776
4090
|
import xtermHeadless from "@xterm/headless";
|
|
3777
4091
|
import xtermSerialize from "@xterm/addon-serialize";
|
|
3778
4092
|
var { Terminal } = xtermHeadless;
|
|
@@ -3866,12 +4180,12 @@ var AgentTerminal = class {
|
|
|
3866
4180
|
async save(file) {
|
|
3867
4181
|
this.dirty = false;
|
|
3868
4182
|
await this.flush();
|
|
3869
|
-
await
|
|
3870
|
-
await
|
|
3871
|
-
await
|
|
4183
|
+
await fs10.mkdir(path12.dirname(file), { recursive: true });
|
|
4184
|
+
await fs10.writeFile(`${file}.tmp`, this.snapshot());
|
|
4185
|
+
await fs10.rename(`${file}.tmp`, file);
|
|
3872
4186
|
}
|
|
3873
4187
|
async load(file) {
|
|
3874
|
-
const data = await
|
|
4188
|
+
const data = await fs10.readFile(file, "utf8").catch(() => "");
|
|
3875
4189
|
if (!data) return;
|
|
3876
4190
|
this.term.write(data);
|
|
3877
4191
|
this.note("(the office restarted)");
|
|
@@ -4001,16 +4315,15 @@ var FEMININE_NAMES = new Set(
|
|
|
4001
4315
|
);
|
|
4002
4316
|
var lookFor = (name) => FEMININE_NAMES.has(name.trim().split(/\s+/)[0].toLowerCase()) ? "feminine" : "masculine";
|
|
4003
4317
|
var LOOKS = ["feminine", "masculine"];
|
|
4004
|
-
var MAX_DESKS = {
|
|
4318
|
+
var MAX_DESKS = { ...FLOOR_DESKS, ceo: 1 };
|
|
4005
4319
|
var MAX_QA_ROUNDS = 3;
|
|
4006
4320
|
var DEFAULT_MODEL = "claude-opus-5-5";
|
|
4007
4321
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
4008
4322
|
var CEO_MODEL = "claude-opus-5-5";
|
|
4009
4323
|
var CEO_EFFORT = "xhigh";
|
|
4010
4324
|
var CEO_NAME = "Morgan";
|
|
4011
|
-
var CEO_DIR =
|
|
4012
|
-
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(
|
|
4013
|
-
var MAX_PENDING_REQUESTS = 8;
|
|
4325
|
+
var CEO_DIR = path13.join(HOME_DIR, "ceo");
|
|
4326
|
+
var DEFAULT_PROJECTS_DIR = defaultProjectsDir(path13.resolve(import.meta.dirname, ".."));
|
|
4014
4327
|
var MAX_ISSUES_PER_JOB = 12;
|
|
4015
4328
|
var KEEP_MESSAGES = 200;
|
|
4016
4329
|
var KEEP_DECIDED_REQUESTS = 40;
|
|
@@ -4022,14 +4335,14 @@ var ERROR_COOLDOWN_MS = 2 * 6e4;
|
|
|
4022
4335
|
var MAX_ISSUE_FAILURES = 2;
|
|
4023
4336
|
var LIMIT_PAUSE_MS = 15 * 6e4;
|
|
4024
4337
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).split(/\r?\n/)[0].slice(0, 200);
|
|
4025
|
-
var SCREENS_DIR =
|
|
4338
|
+
var SCREENS_DIR = path13.join(HOME_DIR, "screens");
|
|
4026
4339
|
var MIME_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/svg+xml": "svg" };
|
|
4027
|
-
var screenFile = (agentId, mime) =>
|
|
4340
|
+
var screenFile = (agentId, mime) => path13.join(SCREENS_DIR, `${agentId}.${MIME_EXT[mime] ?? "img"}`);
|
|
4028
4341
|
async function loadScreen(agentId) {
|
|
4029
4342
|
for (const [mime, ext] of Object.entries(MIME_EXT)) {
|
|
4030
|
-
const file =
|
|
4343
|
+
const file = path13.join(SCREENS_DIR, `${agentId}.${ext}`);
|
|
4031
4344
|
try {
|
|
4032
|
-
const [data, stat] = await Promise.all([
|
|
4345
|
+
const [data, stat] = await Promise.all([fs11.readFile(file), fs11.stat(file)]);
|
|
4033
4346
|
return { data, mime, at: stat.mtimeMs };
|
|
4034
4347
|
} catch {
|
|
4035
4348
|
}
|
|
@@ -4037,10 +4350,10 @@ async function loadScreen(agentId) {
|
|
|
4037
4350
|
return null;
|
|
4038
4351
|
}
|
|
4039
4352
|
async function removeScreens(agentId) {
|
|
4040
|
-
await Promise.all(Object.values(MIME_EXT).map((ext) =>
|
|
4353
|
+
await Promise.all(Object.values(MIME_EXT).map((ext) => fs11.rm(path13.join(SCREENS_DIR, `${agentId}.${ext}`), { force: true })));
|
|
4041
4354
|
}
|
|
4042
|
-
var TERMINALS_DIR =
|
|
4043
|
-
var terminalFile = (agentId) =>
|
|
4355
|
+
var TERMINALS_DIR = path13.join(HOME_DIR, "terminals");
|
|
4356
|
+
var terminalFile = (agentId) => path13.join(TERMINALS_DIR, `${agentId}.ansi`);
|
|
4044
4357
|
var TERMINAL_SAVE_MS = 2e4;
|
|
4045
4358
|
var QA_SCHEMA = {
|
|
4046
4359
|
type: "object",
|
|
@@ -4194,7 +4507,7 @@ var Swarm = class {
|
|
|
4194
4507
|
// ---------- lifecycle ----------
|
|
4195
4508
|
async init() {
|
|
4196
4509
|
try {
|
|
4197
|
-
const raw = await
|
|
4510
|
+
const raw = await fs11.readFile(STATE_FILE, "utf8");
|
|
4198
4511
|
const loaded = JSON.parse(raw);
|
|
4199
4512
|
this.state = {
|
|
4200
4513
|
settings: { ...this.state.settings, ...loaded.settings },
|
|
@@ -4320,6 +4633,7 @@ var Swarm = class {
|
|
|
4320
4633
|
setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
|
|
4321
4634
|
setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
|
|
4322
4635
|
setInterval(() => void this.saveTerminals(), TERMINAL_SAVE_MS);
|
|
4636
|
+
setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.sweepFloor(r.id), i * 1500)), DESK_SWEEP_INTERVAL_MS);
|
|
4323
4637
|
void this.backend.detectClis().then((clis) => {
|
|
4324
4638
|
this.clis = clis;
|
|
4325
4639
|
this.broadcast({ type: "clis", clis });
|
|
@@ -4515,10 +4829,10 @@ var Swarm = class {
|
|
|
4515
4829
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
4516
4830
|
this.saveTimer = null;
|
|
4517
4831
|
for (const a of this.state.agents) a.logTail = (this.agentRt.get(a.id)?.log ?? []).slice(-200);
|
|
4518
|
-
await
|
|
4832
|
+
await fs11.mkdir(path13.dirname(STATE_FILE), { recursive: true });
|
|
4519
4833
|
const tmp = `${STATE_FILE}.tmp`;
|
|
4520
|
-
await
|
|
4521
|
-
await
|
|
4834
|
+
await fs11.writeFile(tmp, JSON.stringify(this.state, null, 2));
|
|
4835
|
+
await fs11.rename(tmp, STATE_FILE);
|
|
4522
4836
|
}
|
|
4523
4837
|
// ---------- lookups ----------
|
|
4524
4838
|
repo(id) {
|
|
@@ -4600,7 +4914,7 @@ var Swarm = class {
|
|
|
4600
4914
|
const name = fullName.split("/")[1];
|
|
4601
4915
|
const taken = folders.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
4602
4916
|
if (taken) throw new HttpError(409, `${taken.path} already exists and isn't a clone of ${fullName}. Connect that folder instead, or rename it first.`);
|
|
4603
|
-
return
|
|
4917
|
+
return path13.join(root, name);
|
|
4604
4918
|
}
|
|
4605
4919
|
/** A brand-new project: a folder in your projects folder, pushed to a new GitHub repo. */
|
|
4606
4920
|
async createRepo(name, opts) {
|
|
@@ -4612,11 +4926,11 @@ var Swarm = class {
|
|
|
4612
4926
|
}
|
|
4613
4927
|
/** The folders in your projects folder (or another folder you point at), and which are floors already. */
|
|
4614
4928
|
async listProjectFolders(dir) {
|
|
4615
|
-
const root = dir?.trim() ?
|
|
4929
|
+
const root = dir?.trim() ? path13.resolve(dir.trim()) : this.state.settings.projectsDir;
|
|
4616
4930
|
const folders = await this.backend.scanProjects(root).catch((err) => {
|
|
4617
4931
|
throw new HttpError(400, err.message);
|
|
4618
4932
|
});
|
|
4619
|
-
const same = (a, b) =>
|
|
4933
|
+
const same = (a, b) => path13.resolve(a).toLowerCase() === path13.resolve(b).toLowerCase();
|
|
4620
4934
|
const floorOf = (f) => this.state.repos.find((r) => r.localPath && same(r.localPath, f.path) || f.github && r.id.toLowerCase() === f.github.toLowerCase())?.floor ?? null;
|
|
4621
4935
|
return { root, folders: folders.map((f) => ({ name: f.name, path: f.path, git: f.git, github: f.github, floor: floorOf(f) })) };
|
|
4622
4936
|
}
|
|
@@ -4762,8 +5076,29 @@ var Swarm = class {
|
|
|
4762
5076
|
return this.mergeNote(rec, `merge blocked: ${error}`);
|
|
4763
5077
|
}
|
|
4764
5078
|
this.toast("success", `\u{1F500} Merged PR #${pr.number} into ${repo.defaultBranch}: ${pr.title}`);
|
|
5079
|
+
await this.closeResolvedIssues(repo, pr.number, pr);
|
|
4765
5080
|
return true;
|
|
4766
5081
|
}
|
|
5082
|
+
/**
|
|
5083
|
+
* After the office merges a PR: close the issues it resolves now rather than waiting for GitHub's "Closes #N" (which
|
|
5084
|
+
* lags, and never comes when the link wasn't recognised), so the scheduler doesn't hand them out again. Never throws.
|
|
5085
|
+
*/
|
|
5086
|
+
async closeResolvedIssues(repo, prNumber, pr) {
|
|
5087
|
+
const numbers = issuesResolvedBy(pr);
|
|
5088
|
+
const rt = this.repoRt.get(repo.id);
|
|
5089
|
+
if (rt && rt.issues.some((i) => numbers.includes(i.number))) {
|
|
5090
|
+
rt.issues = rt.issues.filter((i) => !numbers.includes(i.number));
|
|
5091
|
+
this.emitRepo(repo);
|
|
5092
|
+
}
|
|
5093
|
+
for (const n of numbers) {
|
|
5094
|
+
try {
|
|
5095
|
+
if (await this.backend.issueState(repo.fullName, n) !== "OPEN") continue;
|
|
5096
|
+
await this.backend.closeIssue(repo.fullName, n, `Shipped in #${prNumber}`);
|
|
5097
|
+
} catch (err) {
|
|
5098
|
+
console.warn(`could not close ${repo.fullName}#${n} after merging #${prNumber}`, err);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
4767
5102
|
/** Show where auto-merge stands on a PR's card. Doesn't count as a change to the record. */
|
|
4768
5103
|
mergeNote(rec, text) {
|
|
4769
5104
|
if (rec.mergeNote !== text) {
|
|
@@ -4796,7 +5131,7 @@ var Swarm = class {
|
|
|
4796
5131
|
rt.folderSync = status;
|
|
4797
5132
|
this.emitRepo(repo);
|
|
4798
5133
|
if (status && status !== before) {
|
|
4799
|
-
const name = repo.localPath ?
|
|
5134
|
+
const name = repo.localPath ? path13.basename(repo.localPath) : repo.fullName;
|
|
4800
5135
|
if (status.startsWith("updated")) this.toast("success", `\u{1F4C1} ${name}: ${status}`);
|
|
4801
5136
|
else if (/behind|diverged|failed/.test(status)) this.toast("info", `\u{1F4C1} ${name} wasn't updated: ${status}`);
|
|
4802
5137
|
}
|
|
@@ -4806,6 +5141,31 @@ var Swarm = class {
|
|
|
4806
5141
|
async syncFolderNow(repoId2) {
|
|
4807
5142
|
return { folderSync: await this.syncFolder(this.repo(repoId2)) };
|
|
4808
5143
|
}
|
|
5144
|
+
/** Remove the floor's desks, desk folders and swarm/qa branches nobody uses any more; say so when something went. */
|
|
5145
|
+
async sweepFloor(repoId2) {
|
|
5146
|
+
const repo = this.state.repos.find((r) => r.id === repoId2);
|
|
5147
|
+
const rt = this.repoRt.get(repoId2);
|
|
5148
|
+
if (!repo || !rt || rt.cloneStatus !== "ready") return;
|
|
5149
|
+
try {
|
|
5150
|
+
const pulls = rt.lastSync ? rt.pulls : await this.backend.listPulls(repo.fullName);
|
|
5151
|
+
const agents = this.state.agents.filter((a) => a.repoId === repo.id);
|
|
5152
|
+
const keep = {
|
|
5153
|
+
desks: [...agents.map((a) => this.agentSlug(a)), PREVIEW_SLUG],
|
|
5154
|
+
branches: [...agents.flatMap((a) => a.branch ? [a.branch] : []), ...pulls.filter((p) => p.state === "OPEN").map((p) => p.headRefName)]
|
|
5155
|
+
};
|
|
5156
|
+
const r = await this.backend.sweepDesks(repo.fullName, keep);
|
|
5157
|
+
const desks = r.desks + r.folders;
|
|
5158
|
+
const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${path13.dirname(r.patches[0])})` : "";
|
|
5159
|
+
console.log(`desk sweep ${repo.fullName}: ${desks} desks, ${r.branches} branches removed${saved}${r.skipped.length ? `, ${r.skipped.length} still in use` : ""}`);
|
|
5160
|
+
if (!desks && !r.branches) return;
|
|
5161
|
+
const removed = [desks && `${desks} old desk${desks === 1 ? "" : "s"}`, r.branches && `${r.branches} finished branch${r.branches === 1 ? "" : "es"}`].filter(Boolean).join(" and ");
|
|
5162
|
+
const text = `\u{1F9F9} ${repo.fullName.split("/")[1]}: removed ${removed}${saved}`;
|
|
5163
|
+
this.toast("info", text);
|
|
5164
|
+
this.postMessage("office", text);
|
|
5165
|
+
} catch (err) {
|
|
5166
|
+
console.warn(`desk sweep ${repo.fullName} failed: ${oneLine(err)}`);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
4809
5169
|
// ---------- the floor's app (preview monitor) ----------
|
|
4810
5170
|
/** Run the floor's app from its preview worktree: the default branch, or an open PR. Replaces what it is running now. */
|
|
4811
5171
|
async startPreview(id, pr) {
|
|
@@ -4836,6 +5196,7 @@ var Swarm = class {
|
|
|
4836
5196
|
await this.backend.ensureClone(repo.fullName);
|
|
4837
5197
|
rt.cloneStatus = "ready";
|
|
4838
5198
|
void this.previews.refreshDefault(repo);
|
|
5199
|
+
void this.sweepFloor(id);
|
|
4839
5200
|
} catch (err) {
|
|
4840
5201
|
rt.cloneStatus = "error";
|
|
4841
5202
|
rt.cloneError = err.message;
|
|
@@ -4911,8 +5272,10 @@ var Swarm = class {
|
|
|
4911
5272
|
}
|
|
4912
5273
|
async mergePull(repoId2, number, method = "squash") {
|
|
4913
5274
|
const repo = this.repo(repoId2);
|
|
5275
|
+
const pr = this.repoRt.get(repo.id)?.pulls.find((p) => p.number === number) ?? await this.backend.prDetails(repo.fullName, number);
|
|
4914
5276
|
await this.backend.mergePull(repo.fullName, number, method);
|
|
4915
5277
|
this.toast("success", `Merged PR #${number} into ${repo.defaultBranch}`);
|
|
5278
|
+
await this.closeResolvedIssues(repo, number, pr);
|
|
4916
5279
|
await this.syncRepo(repo.id);
|
|
4917
5280
|
setTimeout(() => this.schedule(), 200);
|
|
4918
5281
|
}
|
|
@@ -5020,7 +5383,7 @@ var Swarm = class {
|
|
|
5020
5383
|
this.agentRt.get(id)?.terminal?.releaseIdle?.();
|
|
5021
5384
|
this.agentRt.get(id)?.terminal?.dispose();
|
|
5022
5385
|
void removeScreens(id);
|
|
5023
|
-
void
|
|
5386
|
+
void fs11.rm(terminalFile(id), { force: true }).catch(() => void 0);
|
|
5024
5387
|
for (const q of this.state.qa) {
|
|
5025
5388
|
if (q.qaAgentId === id && q.status === "testing") this.setQa(q, { status: "queued", qaAgentId: null });
|
|
5026
5389
|
if (q.devAgentId === id) q.devAgentId = null;
|
|
@@ -5029,7 +5392,8 @@ var Swarm = class {
|
|
|
5029
5392
|
const repo = this.state.repos.find((r) => r.id === a.repoId);
|
|
5030
5393
|
if (repo) {
|
|
5031
5394
|
const slug = this.agentSlug(a);
|
|
5032
|
-
|
|
5395
|
+
const main = this.backend.mainDir(repo.fullName);
|
|
5396
|
+
void this.backend.releaseDesk(repo.fullName, slug, this.port(a)).then(() => this.backend.removeDesk(repo.fullName, slug, main)).catch(() => void 0).then(() => this.sweepFloor(repo.id));
|
|
5033
5397
|
}
|
|
5034
5398
|
this.state.agents = this.state.agents.filter((x) => x.id !== id);
|
|
5035
5399
|
this.agentRt.delete(id);
|
|
@@ -5073,9 +5437,12 @@ var Swarm = class {
|
|
|
5073
5437
|
this.save();
|
|
5074
5438
|
}
|
|
5075
5439
|
issueTaken(repo, n) {
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5440
|
+
return issueTaken(
|
|
5441
|
+
n,
|
|
5442
|
+
this.state.agents.filter((a) => a.repoId === repo.id),
|
|
5443
|
+
this.repoRt.get(repo.id)?.pulls ?? [],
|
|
5444
|
+
Date.now()
|
|
5445
|
+
);
|
|
5079
5446
|
}
|
|
5080
5447
|
ensureSlot() {
|
|
5081
5448
|
if (this.slotsFull()) {
|
|
@@ -5123,7 +5490,7 @@ var Swarm = class {
|
|
|
5123
5490
|
}
|
|
5124
5491
|
// ---------- terminals ----------
|
|
5125
5492
|
async loadTerminal(agentId) {
|
|
5126
|
-
if (!await
|
|
5493
|
+
if (!await fs11.stat(terminalFile(agentId)).catch(() => null)) return null;
|
|
5127
5494
|
const t = this.newTerminal(agentId);
|
|
5128
5495
|
await t.load(terminalFile(agentId));
|
|
5129
5496
|
return t;
|
|
@@ -5154,34 +5521,18 @@ var Swarm = class {
|
|
|
5154
5521
|
t.attach(ws);
|
|
5155
5522
|
}
|
|
5156
5523
|
buildSystemAppend(a, repo, cwd, branch, fixing) {
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
`Your worktree: ${cwd}`,
|
|
5170
|
-
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})`,
|
|
5171
|
-
linked.length ? `Related repositories you may read for context (do not modify them):
|
|
5172
|
-
${linked.join("\n")}` : "",
|
|
5173
|
-
"",
|
|
5174
|
-
"Workflow:",
|
|
5175
|
-
"1. Read the issue and explore the relevant code before changing anything.",
|
|
5176
|
-
"2. Implement the change with focused commits and clear messages.",
|
|
5177
|
-
"3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed.",
|
|
5178
|
-
repo.browserTesting ? `4. If the project has a web UI, start its dev server in the background on port ${this.port(a)} (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.",
|
|
5179
|
-
`5. Push: ${push}`,
|
|
5180
|
-
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>".`,
|
|
5181
|
-
fixing ? "" : "7. End your final message with the pull request URL on its own line.",
|
|
5182
|
-
"",
|
|
5183
|
-
"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."
|
|
5184
|
-
].filter((l) => l !== "").join("\n");
|
|
5524
|
+
return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing });
|
|
5525
|
+
}
|
|
5526
|
+
linkedDirs(repo) {
|
|
5527
|
+
return this.linkedRepos(repo).map((r) => ({ fullName: r.fullName, dir: this.backend.mainDir(r.fullName) }));
|
|
5528
|
+
}
|
|
5529
|
+
/** What an agent is told on a task, previewed with placeholders for the task's details. */
|
|
5530
|
+
agentPrompt(id) {
|
|
5531
|
+
const a = this.agent(id);
|
|
5532
|
+
if (a.role === "ceo") return ceoPromptPreview(ceoSystemPrompt(this.ceoPromptInput(a)));
|
|
5533
|
+
const repo = this.repo(a.repoId);
|
|
5534
|
+
const base = { agent: a, repo, port: this.port(a), slug: slugify(a.name) };
|
|
5535
|
+
return a.role === "qa" ? qaPromptPreview(base) : devPromptPreview({ ...base, linked: this.linkedDirs(repo) });
|
|
5185
5536
|
}
|
|
5186
5537
|
beginTask(a, patch, banner, preparing) {
|
|
5187
5538
|
const rt = this.agentRt.get(a.id);
|
|
@@ -5224,7 +5575,7 @@ ${linked.join("\n")}` : "",
|
|
|
5224
5575
|
}
|
|
5225
5576
|
}
|
|
5226
5577
|
async runTask(a, repo, issue2, note) {
|
|
5227
|
-
const branch =
|
|
5578
|
+
const branch = devBranch(issue2.number, slugify(a.name));
|
|
5228
5579
|
this.beginTask(
|
|
5229
5580
|
a,
|
|
5230
5581
|
{ task: "issue", issueNumber: issue2.number, issueTitle: issue2.title, branch, prNumber: null, prUrl: null, sessionId: null },
|
|
@@ -5289,7 +5640,7 @@ Note from the manager: ${note}` : ""
|
|
|
5289
5640
|
rt.shots.push({ data, mime, url: rt.browserUrl, at });
|
|
5290
5641
|
if (rt.shots.length > 12) rt.shots.shift();
|
|
5291
5642
|
this.broadcast({ type: "screen", agentId: a.id, url: rt.browserUrl, at });
|
|
5292
|
-
void removeScreens(a.id).then(() =>
|
|
5643
|
+
void removeScreens(a.id).then(() => fs11.mkdir(SCREENS_DIR, { recursive: true })).then(() => fs11.writeFile(screenFile(a.id, mime), data)).catch((err) => console.warn("could not save screenshot", err));
|
|
5293
5644
|
},
|
|
5294
5645
|
limited: (at) => this.pauseForLimit(at),
|
|
5295
5646
|
usageWarning: (info) => this.paceForWarning(info),
|
|
@@ -5426,32 +5777,10 @@ Note from the manager: ${note}` : ""
|
|
|
5426
5777
|
this.toast("info", `PR #${prNumber} is queued for QA`);
|
|
5427
5778
|
}
|
|
5428
5779
|
buildQaSystemAppend(a, repo, cwd, branch, pr) {
|
|
5429
|
-
return
|
|
5430
|
-
`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.`,
|
|
5431
|
-
...a.brief ? ["", `Your job description:
|
|
5432
|
-
${a.brief}`] : [],
|
|
5433
|
-
...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."] : [],
|
|
5434
|
-
"",
|
|
5435
|
-
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
5436
|
-
...repo.summary ? [`Project: ${repo.summary}`] : [],
|
|
5437
|
-
...repo.mission ? [`What the team is building (the manager's brief): ${repo.mission}`] : [],
|
|
5438
|
-
...repo.qaBrief ? [`What to check on this project (from the CEO):
|
|
5439
|
-
${repo.qaBrief}`] : [],
|
|
5440
|
-
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
5441
|
-
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
5442
|
-
"",
|
|
5443
|
-
"How to test:",
|
|
5444
|
-
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
5445
|
-
`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.`,
|
|
5446
|
-
"3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
5447
|
-
repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${this.port(a)} (reserved for you) and exercise the change in a real browser with the Playwright tools: navigate, click, type, resize to a phone size, try edge cases, and check the console for errors. Take a screenshot with browser_take_screenshot (no filename) of every important state: the screenshots are attached to the PR as evidence. Stop the server afterwards.` : "4. Exercise the changed behaviour directly (run the program, call the API, write a quick script).",
|
|
5448
|
-
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
5449
|
-
"",
|
|
5450
|
-
"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."
|
|
5451
|
-
].join("\n");
|
|
5780
|
+
return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr });
|
|
5452
5781
|
}
|
|
5453
5782
|
async runQa(a, repo, rec) {
|
|
5454
|
-
const branch =
|
|
5783
|
+
const branch = qaBranch(rec.prNumber, slugify(a.name));
|
|
5455
5784
|
this.setQa(rec, { status: "testing", qaAgentId: a.id });
|
|
5456
5785
|
this.beginTask(
|
|
5457
5786
|
a,
|
|
@@ -5745,7 +6074,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5745
6074
|
if (patch.ceoHeartbeatMin !== void 0) s.ceoHeartbeatMin = Math.max(0, Math.min(1440, Math.round(Number(patch.ceoHeartbeatMin)) || 0));
|
|
5746
6075
|
if (typeof patch.managerName === "string") s.managerName = patch.managerName.trim().slice(0, 40);
|
|
5747
6076
|
if (typeof patch.companyName === "string") s.companyName = patch.companyName.trim().slice(0, 60);
|
|
5748
|
-
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir =
|
|
6077
|
+
if (typeof patch.projectsDir === "string" && patch.projectsDir.trim()) s.projectsDir = path13.resolve(patch.projectsDir.trim());
|
|
5749
6078
|
if (typeof patch.setupDone === "boolean") s.setupDone = patch.setupDone;
|
|
5750
6079
|
if (patch.tutorialStep !== void 0) s.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
|
|
5751
6080
|
if (typeof patch.autoUpdate === "boolean") s.autoUpdate = patch.autoUpdate;
|
|
@@ -5967,11 +6296,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
5967
6296
|
}
|
|
5968
6297
|
emitOfficeUpdate() {
|
|
5969
6298
|
if (!this.officeHead) return;
|
|
5970
|
-
const
|
|
5971
|
-
const key = JSON.stringify(
|
|
6299
|
+
const view2 = this.officeUpdateView();
|
|
6300
|
+
const key = JSON.stringify(view2);
|
|
5972
6301
|
if (key === this.lastOfficeView) return;
|
|
5973
6302
|
this.lastOfficeView = key;
|
|
5974
|
-
this.broadcast({ type: "officeUpdate", officeUpdate:
|
|
6303
|
+
this.broadcast({ type: "officeUpdate", officeUpdate: view2 });
|
|
5975
6304
|
}
|
|
5976
6305
|
/** The own-folder check found the office this many commits behind GitHub. */
|
|
5977
6306
|
setOfficeBehind(behind) {
|
|
@@ -6125,11 +6454,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6125
6454
|
}
|
|
6126
6455
|
ceoInfo() {
|
|
6127
6456
|
const c = this.state.ceo;
|
|
6128
|
-
const
|
|
6457
|
+
const view2 = (j) => ({ kind: j.kind, label: jobLabel(j, this.ceoFloor(j.repoId)) });
|
|
6129
6458
|
const min = this.state.settings.ceoHeartbeatMin;
|
|
6130
6459
|
return {
|
|
6131
|
-
queue: c.queue.map(
|
|
6132
|
-
job: c.job ?
|
|
6460
|
+
queue: c.queue.map(view2),
|
|
6461
|
+
job: c.job ? view2(c.job) : null,
|
|
6133
6462
|
lastReviewAt: c.lastReviewAt,
|
|
6134
6463
|
nextReviewAt: min > 0 && c.lastReviewAt ? c.lastReviewAt + min * 6e4 : null
|
|
6135
6464
|
};
|
|
@@ -6173,6 +6502,10 @@ ${job.text}` } : job;
|
|
|
6173
6502
|
}
|
|
6174
6503
|
void this.runCeoJob(a, job);
|
|
6175
6504
|
}
|
|
6505
|
+
ceoPromptInput(a) {
|
|
6506
|
+
const s = this.state.settings;
|
|
6507
|
+
return { name: a.name, company: s.companyName, manager: s.managerName, notesFile: path13.join(CEO_DIR, "NOTES.md"), sessionLimit: s.sessionLimit, teamCap: s.teamCap, hiring: s.hiring };
|
|
6508
|
+
}
|
|
6176
6509
|
async runCeoJob(a, job) {
|
|
6177
6510
|
const rt = this.agentRt.get(a.id);
|
|
6178
6511
|
const floor = this.ceoFloor(job.repoId);
|
|
@@ -6192,27 +6525,18 @@ ${job.text}` } : job;
|
|
|
6192
6525
|
this.emitAgent(a);
|
|
6193
6526
|
this.emitCeo();
|
|
6194
6527
|
this.save();
|
|
6195
|
-
await
|
|
6528
|
+
await fs11.mkdir(CEO_DIR, { recursive: true }).catch(() => void 0);
|
|
6196
6529
|
if (a.status !== "working") {
|
|
6197
6530
|
this.state.ceo.job = null;
|
|
6198
6531
|
this.emitCeo();
|
|
6199
6532
|
return;
|
|
6200
6533
|
}
|
|
6201
|
-
const s = this.state.settings;
|
|
6202
6534
|
const how = this.sessionRuntime(a, job.kind === "chat" ? a.sessionId ?? void 0 : void 0);
|
|
6203
6535
|
rt.session = this.backend.startSession(
|
|
6204
6536
|
{
|
|
6205
6537
|
cwd: CEO_DIR,
|
|
6206
6538
|
prompt: ceoJobPrompt(job, floor),
|
|
6207
|
-
systemAppend: ceoSystemPrompt(
|
|
6208
|
-
name: a.name,
|
|
6209
|
-
company: s.companyName,
|
|
6210
|
-
manager: s.managerName,
|
|
6211
|
-
notesFile: path12.join(CEO_DIR, "NOTES.md"),
|
|
6212
|
-
sessionLimit: s.sessionLimit,
|
|
6213
|
-
teamCap: s.teamCap,
|
|
6214
|
-
hiring: s.hiring
|
|
6215
|
-
}),
|
|
6539
|
+
systemAppend: ceoSystemPrompt(this.ceoPromptInput(a)),
|
|
6216
6540
|
model: a.model || CEO_MODEL,
|
|
6217
6541
|
effort: a.effort || CEO_EFFORT,
|
|
6218
6542
|
browserTesting: false,
|
|
@@ -6435,6 +6759,12 @@ ${t}`);
|
|
|
6435
6759
|
const mark = `\u2026 (truncated, ${brief.length} chars; see agent_detail)`;
|
|
6436
6760
|
return brief.slice(0, 400 - mark.length).trimEnd() + mark;
|
|
6437
6761
|
};
|
|
6762
|
+
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6763
|
+
const seats = (r, role) => seatCount(
|
|
6764
|
+
MAX_DESKS[role],
|
|
6765
|
+
this.state.agents.filter((a) => a.repoId === r.id && a.role === role).length,
|
|
6766
|
+
pending2.filter((p) => p.kind === "hire" && p.repoId === r.id && p.role === role).length
|
|
6767
|
+
);
|
|
6438
6768
|
const floors = [...this.state.repos].sort((x, y) => x.floor - y.floor).map((r) => {
|
|
6439
6769
|
const rt = this.repoRt.get(r.id);
|
|
6440
6770
|
const open = new Set(rt.issues.map((i) => i.number));
|
|
@@ -6450,6 +6780,7 @@ ${t}`);
|
|
|
6450
6780
|
autoAssign: r.autoAssign,
|
|
6451
6781
|
autoMerge: r.autoMerge,
|
|
6452
6782
|
folderSync: rt.folderSync,
|
|
6783
|
+
seats: { dev: seats(r, "dev"), qa: seats(r, "qa") },
|
|
6453
6784
|
capacity: {
|
|
6454
6785
|
developers: this.state.agents.filter((a) => a.repoId === r.id && a.role === "dev").length,
|
|
6455
6786
|
developersFree: this.available(r, "dev").length,
|
|
@@ -6503,10 +6834,11 @@ ${t}`);
|
|
|
6503
6834
|
sessionLimit: s.sessionLimit || "none",
|
|
6504
6835
|
sessionsRunning: this.running(),
|
|
6505
6836
|
usage: usageLabel(this.usageNow(), Date.now()),
|
|
6506
|
-
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa }
|
|
6837
|
+
deskLimits: { dev: MAX_DESKS.dev, qa: MAX_DESKS.qa },
|
|
6838
|
+
proposalLimit: { pending: pending2.length, max: MAX_PENDING_PROPOSALS }
|
|
6507
6839
|
},
|
|
6508
6840
|
floors,
|
|
6509
|
-
pendingProposals:
|
|
6841
|
+
pendingProposals: pending2.map(req),
|
|
6510
6842
|
recentDecisions: this.state.requests.filter((r) => r.status !== "pending").slice(-10).map(req)
|
|
6511
6843
|
},
|
|
6512
6844
|
null,
|
|
@@ -6565,7 +6897,7 @@ ${t}`);
|
|
|
6565
6897
|
if (!title) throw new Error("A hire needs a job title.");
|
|
6566
6898
|
const specialty = specialtySlug(x.specialty);
|
|
6567
6899
|
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6568
|
-
|
|
6900
|
+
checkPendingLimit(pending2.length);
|
|
6569
6901
|
const dup = pending2.find((r) => r.kind === "hire" && r.repoId === repo.id && r.role === role && r.specialty === specialty);
|
|
6570
6902
|
if (dup) throw new Error(`${dup.name} (${dup.title}) is already proposed for floor ${repo.floor} with that specialty.`);
|
|
6571
6903
|
const seated = this.state.agents.filter((a) => a.repoId === repo.id && a.role === role).length;
|
|
@@ -6613,7 +6945,7 @@ ${t}`);
|
|
|
6613
6945
|
}
|
|
6614
6946
|
const pending2 = this.state.requests.filter((r) => r.status === "pending");
|
|
6615
6947
|
if (pending2.some((r) => r.kind === "let-go" && r.agentId === a.id)) throw new Error(`Letting ${a.name} go is already proposed.`);
|
|
6616
|
-
|
|
6948
|
+
checkPendingLimit(pending2.length);
|
|
6617
6949
|
const req = {
|
|
6618
6950
|
id: crypto6.randomUUID(),
|
|
6619
6951
|
kind: "let-go",
|
|
@@ -6797,6 +7129,7 @@ app.post("/api/agents/:id/assign", route((req) => swarm.assign(String(req.params
|
|
|
6797
7129
|
app.post("/api/agents/:id/stop", route((req) => swarm.stopAgent(String(req.params.id))));
|
|
6798
7130
|
app.post("/api/agents/:id/reset", route((req) => swarm.resetAgent(String(req.params.id))));
|
|
6799
7131
|
app.post("/api/agents/:id/message", route((req) => swarm.message(String(req.params.id), str(req.body.text))));
|
|
7132
|
+
app.get("/api/agents/:id/prompt", route((req) => swarm.agentPrompt(String(req.params.id))));
|
|
6800
7133
|
app.get("/api/agents/:id/screen", (req, res) => {
|
|
6801
7134
|
const shot = swarm.screenshot(String(req.params.id));
|
|
6802
7135
|
if (!shot) return void res.status(404).end();
|
|
@@ -6814,10 +7147,10 @@ app.post(
|
|
|
6814
7147
|
route((req) => swarm.approveRequest(String(req.params.id), { name: str(req.body?.name) || void 0, model: typeof req.body?.model === "string" ? req.body.model : void 0, effort: typeof req.body?.effort === "string" ? req.body.effort : void 0 }))
|
|
6815
7148
|
);
|
|
6816
7149
|
app.post("/api/requests/:id/reject", route((req) => swarm.rejectRequest(String(req.params.id), str(req.body?.note))));
|
|
6817
|
-
var dist =
|
|
6818
|
-
if (
|
|
7150
|
+
var dist = path14.resolve(import.meta.dirname, "../dist");
|
|
7151
|
+
if (fs12.existsSync(dist)) {
|
|
6819
7152
|
app.use(express.static(dist));
|
|
6820
|
-
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(
|
|
7153
|
+
app.get(/^(?!\/api).*/, (_req, res) => res.sendFile(path14.join(dist, "index.html")));
|
|
6821
7154
|
}
|
|
6822
7155
|
app.use((err, _req, res, _next) => {
|
|
6823
7156
|
const parserStatus = err?.status;
|