cubefarm 0.3.7 → 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/dist/assets/ToyWorld-DAEljc3S.js +2 -0
- package/dist/assets/{index-D3lYbosx.js → index-B7sInPPW.js} +68 -68
- package/dist/assets/{index-hi9vqi9h.css → index-CIr98Zaa.css} +1 -1
- package/dist/assets/{rapier-DfUh5clU.js → rapier-sDtDRX5B.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/index.js +363 -81
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/assets/ToyWorld-DQTMvcEl.js +0 -2
package/dist-server/index.js
CHANGED
|
@@ -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) {
|
|
@@ -2277,6 +2427,7 @@ var realBackend = {
|
|
|
2277
2427
|
createIssue,
|
|
2278
2428
|
issueState,
|
|
2279
2429
|
editIssue,
|
|
2430
|
+
closeIssue,
|
|
2280
2431
|
mergePull,
|
|
2281
2432
|
updateBranch,
|
|
2282
2433
|
closePull,
|
|
@@ -2296,6 +2447,7 @@ var realBackend = {
|
|
|
2296
2447
|
deskDir,
|
|
2297
2448
|
prepareDesk,
|
|
2298
2449
|
removeDesk,
|
|
2450
|
+
sweepDesks,
|
|
2299
2451
|
releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
|
|
2300
2452
|
startSession: (opts, cb) => opts.terminal ? startCliSession(opts, cb) : startSession(opts, cb),
|
|
2301
2453
|
terminals: terminalsAvailable,
|
|
@@ -2697,8 +2849,16 @@ function createDemoBackend() {
|
|
|
2697
2849
|
mergedSinceSync.set(fullName, (mergedSinceSync.get(fullName) ?? 0) + 1);
|
|
2698
2850
|
pr.state = "MERGED";
|
|
2699
2851
|
pr.mergedAt = now();
|
|
2700
|
-
|
|
2701
|
-
|
|
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);
|
|
2702
2862
|
},
|
|
2703
2863
|
updateBranch: async (fullName, number) => {
|
|
2704
2864
|
const pr = repos.get(fullName)?.pulls.find((p) => p.number === number);
|
|
@@ -2753,6 +2913,7 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2753
2913
|
return dir;
|
|
2754
2914
|
},
|
|
2755
2915
|
removeDesk: async () => void 0,
|
|
2916
|
+
sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
|
|
2756
2917
|
releaseDesk: async () => void 0,
|
|
2757
2918
|
startSession: (opts, cb) => {
|
|
2758
2919
|
if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
|
|
@@ -3517,6 +3678,25 @@ var HttpError = class extends Error {
|
|
|
3517
3678
|
status;
|
|
3518
3679
|
};
|
|
3519
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
|
+
|
|
3520
3700
|
// server/mergeGate.ts
|
|
3521
3701
|
var MAX_MERGE_FIXES = 3;
|
|
3522
3702
|
var CHECKS_ALERT_MS = 30 * 6e4;
|
|
@@ -3724,6 +3904,7 @@ var Previews = class {
|
|
|
3724
3904
|
}
|
|
3725
3905
|
/** The floor is leaving the building: stop its preview and remove the preview worktree. */
|
|
3726
3906
|
async remove(f) {
|
|
3907
|
+
const main = this.backend.mainDir(f.fullName);
|
|
3727
3908
|
const r = this.runs.get(f.id);
|
|
3728
3909
|
if (r) {
|
|
3729
3910
|
r.gen++;
|
|
@@ -3732,7 +3913,7 @@ var Previews = class {
|
|
|
3732
3913
|
this.runs.delete(f.id);
|
|
3733
3914
|
this.hasDefault.delete(f.id);
|
|
3734
3915
|
clearTimeout(this.emitTimers.get(f.id));
|
|
3735
|
-
await this.backend.removeDesk(f.fullName, PREVIEW_SLUG).catch(() => void 0);
|
|
3916
|
+
await this.backend.removeDesk(f.fullName, PREVIEW_SLUG, main).catch(() => void 0);
|
|
3736
3917
|
}
|
|
3737
3918
|
/** Server shutdown: stop every running preview. */
|
|
3738
3919
|
async stopAll(floors) {
|
|
@@ -3777,6 +3958,95 @@ var Previews = class {
|
|
|
3777
3958
|
}
|
|
3778
3959
|
};
|
|
3779
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
|
+
|
|
3780
4050
|
// server/pacing.ts
|
|
3781
4051
|
var PACING_MS = 60 * 6e4;
|
|
3782
4052
|
var DEFAULT_PACING_SESSIONS = 3;
|
|
@@ -4363,6 +4633,7 @@ var Swarm = class {
|
|
|
4363
4633
|
setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
|
|
4364
4634
|
setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
|
|
4365
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);
|
|
4366
4637
|
void this.backend.detectClis().then((clis) => {
|
|
4367
4638
|
this.clis = clis;
|
|
4368
4639
|
this.broadcast({ type: "clis", clis });
|
|
@@ -4805,8 +5076,29 @@ var Swarm = class {
|
|
|
4805
5076
|
return this.mergeNote(rec, `merge blocked: ${error}`);
|
|
4806
5077
|
}
|
|
4807
5078
|
this.toast("success", `\u{1F500} Merged PR #${pr.number} into ${repo.defaultBranch}: ${pr.title}`);
|
|
5079
|
+
await this.closeResolvedIssues(repo, pr.number, pr);
|
|
4808
5080
|
return true;
|
|
4809
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
|
+
}
|
|
4810
5102
|
/** Show where auto-merge stands on a PR's card. Doesn't count as a change to the record. */
|
|
4811
5103
|
mergeNote(rec, text) {
|
|
4812
5104
|
if (rec.mergeNote !== text) {
|
|
@@ -4849,6 +5141,31 @@ var Swarm = class {
|
|
|
4849
5141
|
async syncFolderNow(repoId2) {
|
|
4850
5142
|
return { folderSync: await this.syncFolder(this.repo(repoId2)) };
|
|
4851
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
|
+
}
|
|
4852
5169
|
// ---------- the floor's app (preview monitor) ----------
|
|
4853
5170
|
/** Run the floor's app from its preview worktree: the default branch, or an open PR. Replaces what it is running now. */
|
|
4854
5171
|
async startPreview(id, pr) {
|
|
@@ -4879,6 +5196,7 @@ var Swarm = class {
|
|
|
4879
5196
|
await this.backend.ensureClone(repo.fullName);
|
|
4880
5197
|
rt.cloneStatus = "ready";
|
|
4881
5198
|
void this.previews.refreshDefault(repo);
|
|
5199
|
+
void this.sweepFloor(id);
|
|
4882
5200
|
} catch (err) {
|
|
4883
5201
|
rt.cloneStatus = "error";
|
|
4884
5202
|
rt.cloneError = err.message;
|
|
@@ -4954,8 +5272,10 @@ var Swarm = class {
|
|
|
4954
5272
|
}
|
|
4955
5273
|
async mergePull(repoId2, number, method = "squash") {
|
|
4956
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);
|
|
4957
5276
|
await this.backend.mergePull(repo.fullName, number, method);
|
|
4958
5277
|
this.toast("success", `Merged PR #${number} into ${repo.defaultBranch}`);
|
|
5278
|
+
await this.closeResolvedIssues(repo, number, pr);
|
|
4959
5279
|
await this.syncRepo(repo.id);
|
|
4960
5280
|
setTimeout(() => this.schedule(), 200);
|
|
4961
5281
|
}
|
|
@@ -5072,7 +5392,8 @@ var Swarm = class {
|
|
|
5072
5392
|
const repo = this.state.repos.find((r) => r.id === a.repoId);
|
|
5073
5393
|
if (repo) {
|
|
5074
5394
|
const slug = this.agentSlug(a);
|
|
5075
|
-
|
|
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));
|
|
5076
5397
|
}
|
|
5077
5398
|
this.state.agents = this.state.agents.filter((x) => x.id !== id);
|
|
5078
5399
|
this.agentRt.delete(id);
|
|
@@ -5116,9 +5437,12 @@ var Swarm = class {
|
|
|
5116
5437
|
this.save();
|
|
5117
5438
|
}
|
|
5118
5439
|
issueTaken(repo, n) {
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
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
|
+
);
|
|
5122
5446
|
}
|
|
5123
5447
|
ensureSlot() {
|
|
5124
5448
|
if (this.slotsFull()) {
|
|
@@ -5197,34 +5521,18 @@ var Swarm = class {
|
|
|
5197
5521
|
t.attach(ws);
|
|
5198
5522
|
}
|
|
5199
5523
|
buildSystemAppend(a, repo, cwd, branch, fixing) {
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
`Your worktree: ${cwd}`,
|
|
5213
|
-
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})`,
|
|
5214
|
-
linked.length ? `Related repositories you may read for context (do not modify them):
|
|
5215
|
-
${linked.join("\n")}` : "",
|
|
5216
|
-
"",
|
|
5217
|
-
"Workflow:",
|
|
5218
|
-
"1. Read the issue and explore the relevant code before changing anything.",
|
|
5219
|
-
"2. Implement the change with focused commits and clear messages.",
|
|
5220
|
-
"3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed.",
|
|
5221
|
-
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.",
|
|
5222
|
-
`5. Push: ${push}`,
|
|
5223
|
-
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>".`,
|
|
5224
|
-
fixing ? "" : "7. End your final message with the pull request URL on its own line.",
|
|
5225
|
-
"",
|
|
5226
|
-
"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."
|
|
5227
|
-
].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) });
|
|
5228
5536
|
}
|
|
5229
5537
|
beginTask(a, patch, banner, preparing) {
|
|
5230
5538
|
const rt = this.agentRt.get(a.id);
|
|
@@ -5267,7 +5575,7 @@ ${linked.join("\n")}` : "",
|
|
|
5267
5575
|
}
|
|
5268
5576
|
}
|
|
5269
5577
|
async runTask(a, repo, issue2, note) {
|
|
5270
|
-
const branch =
|
|
5578
|
+
const branch = devBranch(issue2.number, slugify(a.name));
|
|
5271
5579
|
this.beginTask(
|
|
5272
5580
|
a,
|
|
5273
5581
|
{ task: "issue", issueNumber: issue2.number, issueTitle: issue2.title, branch, prNumber: null, prUrl: null, sessionId: null },
|
|
@@ -5469,32 +5777,10 @@ Note from the manager: ${note}` : ""
|
|
|
5469
5777
|
this.toast("info", `PR #${prNumber} is queued for QA`);
|
|
5470
5778
|
}
|
|
5471
5779
|
buildQaSystemAppend(a, repo, cwd, branch, pr) {
|
|
5472
|
-
return
|
|
5473
|
-
`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.`,
|
|
5474
|
-
...a.brief ? ["", `Your job description:
|
|
5475
|
-
${a.brief}`] : [],
|
|
5476
|
-
...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."] : [],
|
|
5477
|
-
"",
|
|
5478
|
-
`Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
|
|
5479
|
-
...repo.summary ? [`Project: ${repo.summary}`] : [],
|
|
5480
|
-
...repo.mission ? [`What the team is building (the manager's brief): ${repo.mission}`] : [],
|
|
5481
|
-
...repo.qaBrief ? [`What to check on this project (from the CEO):
|
|
5482
|
-
${repo.qaBrief}`] : [],
|
|
5483
|
-
`Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
|
|
5484
|
-
`Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
|
|
5485
|
-
"",
|
|
5486
|
-
"How to test:",
|
|
5487
|
-
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
5488
|
-
`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.`,
|
|
5489
|
-
"3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
5490
|
-
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).",
|
|
5491
|
-
"5. You may write throwaway scripts to probe behaviour, but do not commit them.",
|
|
5492
|
-
"",
|
|
5493
|
-
"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."
|
|
5494
|
-
].join("\n");
|
|
5780
|
+
return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr });
|
|
5495
5781
|
}
|
|
5496
5782
|
async runQa(a, repo, rec) {
|
|
5497
|
-
const branch =
|
|
5783
|
+
const branch = qaBranch(rec.prNumber, slugify(a.name));
|
|
5498
5784
|
this.setQa(rec, { status: "testing", qaAgentId: a.id });
|
|
5499
5785
|
this.beginTask(
|
|
5500
5786
|
a,
|
|
@@ -6010,11 +6296,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6010
6296
|
}
|
|
6011
6297
|
emitOfficeUpdate() {
|
|
6012
6298
|
if (!this.officeHead) return;
|
|
6013
|
-
const
|
|
6014
|
-
const key = JSON.stringify(
|
|
6299
|
+
const view2 = this.officeUpdateView();
|
|
6300
|
+
const key = JSON.stringify(view2);
|
|
6015
6301
|
if (key === this.lastOfficeView) return;
|
|
6016
6302
|
this.lastOfficeView = key;
|
|
6017
|
-
this.broadcast({ type: "officeUpdate", officeUpdate:
|
|
6303
|
+
this.broadcast({ type: "officeUpdate", officeUpdate: view2 });
|
|
6018
6304
|
}
|
|
6019
6305
|
/** The own-folder check found the office this many commits behind GitHub. */
|
|
6020
6306
|
setOfficeBehind(behind) {
|
|
@@ -6168,11 +6454,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
|
|
|
6168
6454
|
}
|
|
6169
6455
|
ceoInfo() {
|
|
6170
6456
|
const c = this.state.ceo;
|
|
6171
|
-
const
|
|
6457
|
+
const view2 = (j) => ({ kind: j.kind, label: jobLabel(j, this.ceoFloor(j.repoId)) });
|
|
6172
6458
|
const min = this.state.settings.ceoHeartbeatMin;
|
|
6173
6459
|
return {
|
|
6174
|
-
queue: c.queue.map(
|
|
6175
|
-
job: c.job ?
|
|
6460
|
+
queue: c.queue.map(view2),
|
|
6461
|
+
job: c.job ? view2(c.job) : null,
|
|
6176
6462
|
lastReviewAt: c.lastReviewAt,
|
|
6177
6463
|
nextReviewAt: min > 0 && c.lastReviewAt ? c.lastReviewAt + min * 6e4 : null
|
|
6178
6464
|
};
|
|
@@ -6216,6 +6502,10 @@ ${job.text}` } : job;
|
|
|
6216
6502
|
}
|
|
6217
6503
|
void this.runCeoJob(a, job);
|
|
6218
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
|
+
}
|
|
6219
6509
|
async runCeoJob(a, job) {
|
|
6220
6510
|
const rt = this.agentRt.get(a.id);
|
|
6221
6511
|
const floor = this.ceoFloor(job.repoId);
|
|
@@ -6241,21 +6531,12 @@ ${job.text}` } : job;
|
|
|
6241
6531
|
this.emitCeo();
|
|
6242
6532
|
return;
|
|
6243
6533
|
}
|
|
6244
|
-
const s = this.state.settings;
|
|
6245
6534
|
const how = this.sessionRuntime(a, job.kind === "chat" ? a.sessionId ?? void 0 : void 0);
|
|
6246
6535
|
rt.session = this.backend.startSession(
|
|
6247
6536
|
{
|
|
6248
6537
|
cwd: CEO_DIR,
|
|
6249
6538
|
prompt: ceoJobPrompt(job, floor),
|
|
6250
|
-
systemAppend: ceoSystemPrompt(
|
|
6251
|
-
name: a.name,
|
|
6252
|
-
company: s.companyName,
|
|
6253
|
-
manager: s.managerName,
|
|
6254
|
-
notesFile: path13.join(CEO_DIR, "NOTES.md"),
|
|
6255
|
-
sessionLimit: s.sessionLimit,
|
|
6256
|
-
teamCap: s.teamCap,
|
|
6257
|
-
hiring: s.hiring
|
|
6258
|
-
}),
|
|
6539
|
+
systemAppend: ceoSystemPrompt(this.ceoPromptInput(a)),
|
|
6259
6540
|
model: a.model || CEO_MODEL,
|
|
6260
6541
|
effort: a.effort || CEO_EFFORT,
|
|
6261
6542
|
browserTesting: false,
|
|
@@ -6848,6 +7129,7 @@ app.post("/api/agents/:id/assign", route((req) => swarm.assign(String(req.params
|
|
|
6848
7129
|
app.post("/api/agents/:id/stop", route((req) => swarm.stopAgent(String(req.params.id))));
|
|
6849
7130
|
app.post("/api/agents/:id/reset", route((req) => swarm.resetAgent(String(req.params.id))));
|
|
6850
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))));
|
|
6851
7133
|
app.get("/api/agents/:id/screen", (req, res) => {
|
|
6852
7134
|
const shot = swarm.screenshot(String(req.params.id));
|
|
6853
7135
|
if (!shot) return void res.status(404).end();
|