cubefarm 0.3.7 → 0.3.9

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.
@@ -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", "-");
@@ -190,8 +194,10 @@ async function prForBranch(fullName, branch) {
190
194
  const list = await ghJson(["pr", "list", "-R", fullName, "--head", branch, "--state", "all", "--json", "number,url", "--limit", "1"]);
191
195
  return list[0] ?? null;
192
196
  }
197
+ var uniqueNames = (checks) => [...new Set(checks.map(checkName))];
193
198
  async function prDetails(fullName, number) {
194
- const raw = await ghJson(["pr", "view", String(number), "-R", fullName, "--json", "number,title,body,url,headRefName,headRefOid,isCrossRepository,closingIssuesReferences,state,mergeable,mergeStateStatus"]);
199
+ const raw = await ghJson(["pr", "view", String(number), "-R", fullName, "--json", "number,title,body,url,headRefName,headRefOid,isCrossRepository,closingIssuesReferences,state,mergeable,mergeStateStatus,statusCheckRollup"]);
200
+ const rollup = raw.statusCheckRollup ?? [];
195
201
  return {
196
202
  number: raw.number,
197
203
  title: raw.title,
@@ -203,7 +209,11 @@ async function prDetails(fullName, number) {
203
209
  closesIssues: (raw.closingIssuesReferences ?? []).map((r) => r.number),
204
210
  state: raw.state,
205
211
  mergeable: raw.mergeable,
206
- mergeState: raw.mergeStateStatus || "UNKNOWN"
212
+ mergeState: raw.mergeStateStatus || "UNKNOWN",
213
+ checks: checksOf(rollup),
214
+ checkNames: uniqueNames(rollup),
215
+ failedChecks: uniqueNames(rollup.filter(failed)),
216
+ pendingChecks: uniqueNames(rollup.filter(pending))
207
217
  };
208
218
  }
209
219
  async function issueDetails(fullName, number) {
@@ -296,6 +306,33 @@ function npmInstall(dir) {
296
306
  installs.set(dir, next);
297
307
  return next;
298
308
  }
309
+ function porcelainPaths(porcelain) {
310
+ 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);
311
+ }
312
+ function overwrittenPaths(stderr) {
313
+ const lines = stderr.split(/\r?\n/);
314
+ const start = lines.findIndex((l) => /would be overwritten by/.test(l));
315
+ if (start < 0) return [];
316
+ const files = [];
317
+ for (const line of lines.slice(start + 1)) {
318
+ if (!line.startsWith(" ")) break;
319
+ files.push(line.trim());
320
+ }
321
+ return files.filter(Boolean);
322
+ }
323
+ function fileList(files, max = 3, maxChars = 120) {
324
+ const shown = [];
325
+ let length = 0;
326
+ for (const file of files.slice(0, max)) {
327
+ const name = file.length > maxChars ? `\u2026${file.slice(-(maxChars - 1))}` : file;
328
+ const add = name.length + (shown.length ? 2 : 0);
329
+ if (shown.length && length + add > maxChars) break;
330
+ shown.push(name);
331
+ length += add;
332
+ }
333
+ const more = files.length - shown.length;
334
+ return `${shown.join(", ")}${more ? ` +${more} more` : ""}`;
335
+ }
299
336
  async function syncMain(fullName, defaultBranch, opts) {
300
337
  const dir = mainDir(fullName);
301
338
  const result = await withRepoLock(fullName, async () => {
@@ -313,12 +350,17 @@ async function syncMain(fullName, defaultBranch, opts) {
313
350
  if (behind === 0) return stays("in sync");
314
351
  if (Number(await g(["rev-list", "--count", `${target}..HEAD`])) > 0) return stays(`diverged: local commits, and ${commits} to pull`);
315
352
  if (!opts.touch) return { status: `update ready (${commits})`, behind, updatable: true };
316
- if (await g(["status", "--porcelain", "--untracked-files=no"])) return { status: `${behind} behind: local changes`, behind, updatable: true };
353
+ const dirty = await g(["status", "--porcelain", "--untracked-files=no"]);
354
+ if (dirty) {
355
+ const files = porcelainPaths(dirty);
356
+ return { status: `${behind} behind: local changes${files.length ? ` in ${fileList(files)}` : ""}`, behind, updatable: true };
357
+ }
317
358
  const before = await g(["rev-parse", "HEAD"]);
318
359
  try {
319
360
  await g(["merge", "--ff-only", target], 12e4);
320
- } catch {
321
- return { status: `${behind} behind: local files are in the way`, behind, updatable: true };
361
+ } catch (err) {
362
+ const files = overwrittenPaths(err.stderr ?? "");
363
+ return { status: `${behind} behind: local files are in the way${files.length ? `: ${fileList(files)}` : ""}`, behind, updatable: true };
322
364
  }
323
365
  const changed = (await g(["diff", "--name-only", before, "HEAD"])).split(/\r?\n/);
324
366
  const install = changed.some((f) => f === "package.json" || f === "package-lock.json") && await exists(path2.join(dir, "package.json"));
@@ -421,7 +463,32 @@ async function createProject(root, name, opts) {
421
463
  async function removeDir(dir) {
422
464
  await fs2.rm(dir, { recursive: true, force: true, maxRetries: 8, retryDelay: 250 });
423
465
  }
424
- function prepareDesk(fullName, base, agentSlug, branch) {
466
+ var STALE_LOCK_MS = 10 * 6e4;
467
+ async function staleIndexLock(wt) {
468
+ const ref = (await fs2.readFile(path2.join(wt, ".git"), "utf8").catch(() => "")).match(/gitdir:\s*(.+)/)?.[1]?.trim();
469
+ if (!ref) return null;
470
+ const lock = path2.join(path2.resolve(wt, ref), "index.lock");
471
+ const stat = await fs2.stat(lock).catch(() => null);
472
+ return stat && Date.now() - stat.mtimeMs > STALE_LOCK_MS ? lock : null;
473
+ }
474
+ async function reuseDesk(wt, branch, ref) {
475
+ await git(["checkout", "--force", "-B", branch, ref], { cwd: wt });
476
+ await git(["reset", "--hard", ref], { cwd: wt });
477
+ await git(["clean", "-fd"], { cwd: wt }).catch(() => void 0);
478
+ }
479
+ async function clearDeskFolder(wt) {
480
+ try {
481
+ await removeDir(wt);
482
+ } catch (err) {
483
+ const left = await fs2.readdir(wt).catch(() => null);
484
+ if (left?.length !== 0) {
485
+ throw new Error(
486
+ `Could not clear the desk folder ${wt}: ${err.message}. A program started by the previous task is probably still running there. Close it and try again.`
487
+ );
488
+ }
489
+ }
490
+ }
491
+ function prepareDesk(fullName, base, agentSlug, branch, note) {
425
492
  return withRepoLock(fullName, async () => {
426
493
  const main = mainDir(fullName);
427
494
  await git(["fetch", "origin", "--prune"], { cwd: main, timeoutMs: 18e4 });
@@ -440,22 +507,25 @@ function prepareDesk(fullName, base, agentSlug, branch) {
440
507
  const wt = deskDir(fullName, agentSlug);
441
508
  if (await exists(path2.join(wt, ".git"))) {
442
509
  try {
443
- await git(["checkout", "--force", "-B", branch, ref], { cwd: wt });
444
- await git(["reset", "--hard", ref], { cwd: wt });
445
- await git(["clean", "-fd"], { cwd: wt }).catch(() => void 0);
510
+ try {
511
+ await reuseDesk(wt, branch, ref);
512
+ } catch (err) {
513
+ const lock = /index\.lock/.test(err.message) ? await staleIndexLock(wt) : null;
514
+ if (!lock) throw err;
515
+ await fs2.rm(lock, { force: true, maxRetries: 3 });
516
+ note?.(`Removed a stale git lock (${lock}) left in the desk.`);
517
+ await reuseDesk(wt, branch, ref);
518
+ }
446
519
  return wt;
447
- } catch {
520
+ } catch (err) {
521
+ const why = `Couldn't reuse the desk in place, so it's rebuilt: ${err.message}`;
522
+ console.warn(`${wt}: ${why}`);
523
+ note?.(why);
448
524
  }
449
525
  }
450
526
  if (await exists(wt)) {
451
527
  await git(["worktree", "remove", "--force", wt], { cwd: main }).catch(() => void 0);
452
- try {
453
- await removeDir(wt);
454
- } catch (err) {
455
- throw new Error(
456
- `Could not clear the desk folder ${wt}: ${err.message}. A program started by the previous task is probably still running there. Close it and try again.`
457
- );
458
- }
528
+ await clearDeskFolder(wt);
459
529
  }
460
530
  await git(["worktree", "prune"], { cwd: main });
461
531
  await fs2.mkdir(path2.dirname(wt), { recursive: true });
@@ -463,9 +533,8 @@ function prepareDesk(fullName, base, agentSlug, branch) {
463
533
  return wt;
464
534
  });
465
535
  }
466
- function removeDesk(fullName, agentSlug) {
536
+ function removeDesk(fullName, agentSlug, main = mainDir(fullName)) {
467
537
  return withRepoLock(fullName, async () => {
468
- const main = mainDir(fullName);
469
538
  const wt = deskDir(fullName, agentSlug);
470
539
  if (!await exists(wt)) return;
471
540
  await git(["worktree", "remove", "--force", wt], { cwd: main }).catch(() => void 0);
@@ -473,6 +542,121 @@ function removeDesk(fullName, agentSlug) {
473
542
  await git(["worktree", "prune"], { cwd: main }).catch(() => void 0);
474
543
  });
475
544
  }
545
+ var OLD_HOMES = [".office-swarm"];
546
+ var OFFICE_BRANCH = /^(swarm\/issue-|qa\/pr-)/;
547
+ function parseWorktrees(porcelain) {
548
+ const out = [];
549
+ for (const line of porcelain.split(/\r?\n/)) {
550
+ if (line.startsWith("worktree ")) out.push({ path: path2.resolve(line.slice(9)), branch: null, locked: false });
551
+ else if (!out.length) continue;
552
+ else if (line.startsWith("branch ")) out[out.length - 1].branch = line.slice(7).replace(/^refs\/heads\//, "");
553
+ else if (line === "locked" || line.startsWith("locked ")) out[out.length - 1].locked = true;
554
+ }
555
+ return out;
556
+ }
557
+ var samePath = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
558
+ function isOfficeDesk(p, fullName, root) {
559
+ const desks = path2.dirname(p);
560
+ const repo = path2.dirname(desks);
561
+ const workspaces = path2.dirname(repo);
562
+ if (!samePath(path2.basename(desks), "desks") || !samePath(path2.basename(repo), fullName.replace("/", "__"))) return false;
563
+ if (samePath(workspaces, path2.resolve(root))) return true;
564
+ return path2.basename(workspaces) === "workspaces" && OLD_HOMES.some((h) => samePath(path2.basename(path2.dirname(workspaces)), h));
565
+ }
566
+ function planSweep(input, keep) {
567
+ const deskRoot = path2.join(path2.resolve(input.root), input.fullName.replace("/", "__"), "desks");
568
+ const kept = (p) => keep.desks.some((slug) => samePath(p, path2.join(deskRoot, slug)));
569
+ const worktrees = input.worktrees.slice(1).filter((w) => !w.locked && isOfficeDesk(w.path, input.fullName, input.root) && !kept(w.path));
570
+ const stays = input.worktrees.filter((w) => !worktrees.includes(w));
571
+ const folders = input.folders.filter((name) => {
572
+ const p = path2.join(deskRoot, name);
573
+ return !kept(p) && !input.worktrees.some((w) => samePath(w.path, p));
574
+ });
575
+ const branches = input.branches.filter((b) => OFFICE_BRANCH.test(b) && !keep.branches.includes(b) && !stays.some((w) => w.branch === b));
576
+ return { worktrees, folders, branches };
577
+ }
578
+ async function inUse(dir) {
579
+ const probe = `${dir}.sweep`;
580
+ try {
581
+ await fs2.rename(dir, probe);
582
+ } catch (err) {
583
+ return err.code !== "ENOENT";
584
+ }
585
+ await fs2.rename(probe, dir);
586
+ return false;
587
+ }
588
+ var stamp = (d) => `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
589
+ async function saveLeftovers(fullName, desk) {
590
+ const g = (args) => git(args, { cwd: desk, timeoutMs: 6e4 });
591
+ if (!await g(["rev-parse", "--verify", "-q", "HEAD"]).catch(() => "")) return null;
592
+ const dirty = await g(["status", "--porcelain", "--untracked-files=no"]);
593
+ const commits = Number(await g(["rev-list", "--count", "HEAD", "--not", "--remotes"]));
594
+ if (!dirty && !commits) return null;
595
+ const dir = path2.join(HOME_DIR, "leftovers", fullName.replace("/", "__"));
596
+ await fs2.mkdir(dir, { recursive: true });
597
+ const tmp = await fs2.mkdtemp(path2.join(dir, ".tmp-"));
598
+ try {
599
+ if (commits) await g(["format-patch", "-q", "-o", tmp, "HEAD", "--not", "--remotes"]);
600
+ if (dirty) await g(["diff", "HEAD", "--binary", `--output=${path2.join(tmp, "zz-uncommitted.patch")}`]);
601
+ const parts = (await fs2.readdir(tmp)).sort();
602
+ const base = `${path2.basename(desk)}-${stamp(/* @__PURE__ */ new Date())}`;
603
+ let file = path2.join(dir, `${base}.patch`);
604
+ for (let n = 2; await exists(file); n++) file = path2.join(dir, `${base}-${n}.patch`);
605
+ await fs2.writeFile(file, Buffer.concat(await Promise.all(parts.map((p) => fs2.readFile(path2.join(tmp, p))))));
606
+ return file;
607
+ } finally {
608
+ await removeDir(tmp).catch(() => void 0);
609
+ }
610
+ }
611
+ function sweepDesks(fullName, keep) {
612
+ return withRepoLock(fullName, async () => {
613
+ const result = { desks: 0, folders: 0, branches: 0, patches: [], skipped: [] };
614
+ const main = mainDir(fullName);
615
+ if (!await exists(path2.join(main, ".git"))) return result;
616
+ const g = (args) => git(args, { cwd: main, timeoutMs: 6e4 });
617
+ await g(["worktree", "prune"]);
618
+ const real = (p) => fs2.realpath(p).catch(() => p);
619
+ const worktrees = await Promise.all(parseWorktrees(await g(["worktree", "list", "--porcelain"])).map(async (w) => ({ ...w, path: await real(w.path) })));
620
+ const refs = await g(["for-each-ref", "--format=%(refname)", "refs/heads/swarm", "refs/heads/qa"]);
621
+ const branches = refs.split(/\r?\n/).filter(Boolean).map((r) => r.replace(/^refs\/heads\//, ""));
622
+ const root = await real(WORKSPACE_ROOT);
623
+ const deskRoot = path2.join(root, fullName.replace("/", "__"), "desks");
624
+ const folders = (await fs2.readdir(deskRoot, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name);
625
+ const plan = planSweep({ fullName, root, worktrees, branches, folders }, keep);
626
+ const held = /* @__PURE__ */ new Set();
627
+ for (const wt of plan.worktrees) {
628
+ try {
629
+ if (await inUse(wt.path)) throw new Error("in use");
630
+ const patch = await saveLeftovers(fullName, wt.path);
631
+ if (patch) result.patches.push(patch);
632
+ await g(["worktree", "remove", "--force", wt.path]).catch(() => void 0);
633
+ await removeDir(wt.path);
634
+ result.desks++;
635
+ } catch {
636
+ result.skipped.push(wt.path);
637
+ if (wt.branch) held.add(wt.branch);
638
+ }
639
+ }
640
+ await g(["worktree", "prune"]).catch(() => void 0);
641
+ for (const name of plan.folders) {
642
+ const dir = path2.join(deskRoot, name);
643
+ try {
644
+ if (await inUse(dir)) throw new Error("in use");
645
+ await removeDir(dir);
646
+ result.folders++;
647
+ } catch {
648
+ result.skipped.push(dir);
649
+ }
650
+ }
651
+ const doomed = plan.branches.filter((b) => !held.has(b));
652
+ for (let i = 0; i < doomed.length; i += 40) await g(["branch", "-D", ...doomed.slice(i, i + 40)]).catch(() => void 0);
653
+ if (doomed.length) {
654
+ const left = new Set((await g(["for-each-ref", "--format=%(refname)", "refs/heads/swarm", "refs/heads/qa"])).split(/\r?\n/));
655
+ result.branches = doomed.filter((b) => !left.has(`refs/heads/${b}`)).length;
656
+ }
657
+ return result;
658
+ });
659
+ }
476
660
  var LAUNCHERS = ["node.exe", "cmd.exe", "bash.exe", "sh.exe", "conhost.exe", "python.exe", "npm.exe", "npx.exe", "bun.exe", "deno.exe"];
477
661
  var pathForms = (p) => [.../* @__PURE__ */ new Set([p, p.replaceAll("\\", "/"), p.replaceAll("\\", "\\\\")])];
478
662
  function leftoversInDesk(listing, desk, keep) {
@@ -1552,12 +1736,13 @@ function endLive(live) {
1552
1736
  routes.delete(live.token);
1553
1737
  live.term.releaseIdle = null;
1554
1738
  live.term.bind(null);
1555
- const gone = live.proc ? exitOf(live.proc) : void 0;
1739
+ const gone = live.proc ? exitOf(live.proc) : Promise.resolve();
1556
1740
  live.proc?.kill();
1557
1741
  live.proc = null;
1558
1742
  live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
1559
1743
  fs7.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
1560
- return live.cli === "codex" && live.resumeId ? codexThread("archive", live.resumeId, gone) : Promise.resolve();
1744
+ if (live.cli === "codex" && live.resumeId) void codexThread("archive", live.resumeId, gone);
1745
+ return gone;
1561
1746
  }
1562
1747
  var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
1563
1748
  var remember = (l) => l.proc?.setMeta(metaOf(l));
@@ -2277,6 +2462,7 @@ var realBackend = {
2277
2462
  createIssue,
2278
2463
  issueState,
2279
2464
  editIssue,
2465
+ closeIssue,
2280
2466
  mergePull,
2281
2467
  updateBranch,
2282
2468
  closePull,
@@ -2296,6 +2482,7 @@ var realBackend = {
2296
2482
  deskDir,
2297
2483
  prepareDesk,
2298
2484
  removeDesk,
2485
+ sweepDesks,
2299
2486
  releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
2300
2487
  startSession: (opts, cb) => opts.terminal ? startCliSession(opts, cb) : startSession(opts, cb),
2301
2488
  terminals: terminalsAvailable,
@@ -2399,18 +2586,25 @@ function devScript(opts, cb, issueNumber, issueTitle) {
2399
2586
  [{ kind: "tool", tool: "Bash", text: `\u23FA $ gh pr create --title "${issueTitle}" --body "Closes #${issueNumber}"` }]
2400
2587
  ];
2401
2588
  }
2402
- function qaScript(cb, pr, title, round) {
2589
+ function qaScript(cb, pr, title, round, checks) {
2403
2590
  const port = 5600 + pr % 50;
2404
2591
  const hue = pr * 41 % 360;
2405
- return [
2406
- [{ kind: "text", text: `\u25CF Testing PR #${pr} "${title}" (round ${round}). First, the acceptance criteria from the issue.` }],
2407
- [{ kind: "tool", tool: "Bash", text: "\u23FA $ git diff origin/main...HEAD --stat" }, { kind: "result", text: " \u23BF 3 files changed, 82 insertions(+), 9 deletions(-)" }],
2592
+ const verify = checks.endsWith(": none") ? [
2408
2593
  [{ kind: "tool", tool: "Bash", text: "\u23FA $ npm ci && npm test -- --run" }],
2409
2594
  [
2410
2595
  { kind: "result", text: " \u23BF Test Files 8 passed (8)" },
2411
2596
  { kind: "result", text: " Tests 41 passed (41)" }
2412
2597
  ],
2413
- [{ kind: "tool", tool: "Bash", text: "\u23FA $ npm run lint && npm run build" }, { kind: "result", text: " \u23BF \u2713 built in 1.84s" }],
2598
+ [{ kind: "tool", tool: "Bash", text: "\u23FA $ npm run lint && npm run build" }, { kind: "result", text: " \u23BF \u2713 built in 1.84s" }]
2599
+ ] : [
2600
+ [{ kind: "tool", tool: "Read", text: "\u23FA Read .github/workflows/ci.yml" }, { kind: "result", text: " \u23BF Read 41 lines" }],
2601
+ [{ kind: "text", text: "\u25CF CI already runs the tests, lint and build, so I only need a build to start the app." }],
2602
+ [{ kind: "tool", tool: "Bash", text: "\u23FA $ npm ci && npm run build" }, { kind: "result", text: " \u23BF \u2713 built in 1.84s" }]
2603
+ ];
2604
+ return [
2605
+ [{ kind: "text", text: `\u25CF Testing PR #${pr} "${title}" (round ${round}). ${checks}. First, the acceptance criteria from the issue.` }],
2606
+ [{ kind: "tool", tool: "Bash", text: "\u23FA $ git diff origin/main...HEAD --stat" }, { kind: "result", text: " \u23BF 3 files changed, 82 insertions(+), 9 deletions(-)" }],
2607
+ ...verify,
2414
2608
  [{ kind: "tool", tool: "Bash", text: `\u23FA $ npm run preview -- --port ${port} &` }, { kind: "result", text: ` \u23BF Local: http://localhost:${port}/` }],
2415
2609
  () => cb.browserUrl(`http://localhost:${port}/`),
2416
2610
  [{ kind: "tool", tool: "mcp__playwright__browser_navigate", text: `\u23FA \u{1F310} navigate http://localhost:${port}/` }, { kind: "result", text: ` \u23BF Page URL: http://localhost:${port}/` }],
@@ -2446,7 +2640,8 @@ function fakeSession(opts, cb, fullName) {
2446
2640
  { kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort` },
2447
2641
  { kind: "system", text: ` cwd ${opts.cwd}` }
2448
2642
  ];
2449
- const body = kind === "qa" ? qaScript(cb, number, title, round) : kind === "fix" ? fixScript(number, pushes) : devScript(opts, cb, number, title);
2643
+ const checks = opts.prompt.match(/^GitHub checks right now: .*$/m)?.[0] ?? "GitHub checks right now: none";
2644
+ const body = kind === "qa" ? qaScript(cb, number, title, round, checks) : kind === "fix" ? fixScript(number, pushes) : devScript(opts, cb, number, title);
2450
2645
  const script = [header, ...body];
2451
2646
  const finish = () => {
2452
2647
  const costUsd = 0.3 + Math.random();
@@ -2697,8 +2892,16 @@ function createDemoBackend() {
2697
2892
  mergedSinceSync.set(fullName, (mergedSinceSync.get(fullName) ?? 0) + 1);
2698
2893
  pr.state = "MERGED";
2699
2894
  pr.mergedAt = now();
2700
- for (const n of pr.closesIssues) closedIssues.add(`${fullName}#${n}`);
2701
- r.issues = r.issues.filter((i) => !pr.closesIssues.includes(i.number));
2895
+ setTimeout(() => {
2896
+ for (const n of pr.closesIssues) closedIssues.add(`${fullName}#${n}`);
2897
+ r.issues = r.issues.filter((i) => !pr.closesIssues.includes(i.number));
2898
+ }, 2e4).unref();
2899
+ },
2900
+ closeIssue: async (fullName, number) => {
2901
+ const r = repos.get(fullName);
2902
+ if (!r?.issues.some((i) => i.number === number)) throw new Error(`Issue #${number} is not open`);
2903
+ closedIssues.add(`${fullName}#${number}`);
2904
+ r.issues = r.issues.filter((i) => i.number !== number);
2702
2905
  },
2703
2906
  updateBranch: async (fullName, number) => {
2704
2907
  const pr = repos.get(fullName)?.pulls.find((p) => p.number === number);
@@ -2727,7 +2930,11 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
2727
2930
  closesIssues: pr.closesIssues,
2728
2931
  state: pr.state,
2729
2932
  mergeable: pr.mergeable,
2730
- mergeState: pr.mergeState
2933
+ mergeState: pr.mergeState,
2934
+ checks: pr.checks,
2935
+ checkNames: pr.checks === "none" ? [] : ["CI / build", "Vercel"],
2936
+ failedChecks: pr.failedChecks.map((c) => c.name),
2937
+ pendingChecks: pr.pendingChecks
2731
2938
  };
2732
2939
  },
2733
2940
  issueDetails: async (fullName, number) => {
@@ -2753,6 +2960,7 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
2753
2960
  return dir;
2754
2961
  },
2755
2962
  removeDesk: async () => void 0,
2963
+ sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
2756
2964
  releaseDesk: async () => void 0,
2757
2965
  startSession: (opts, cb) => {
2758
2966
  if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
@@ -3488,6 +3696,13 @@ function specialtySlug(s) {
3488
3696
  }
3489
3697
  var specialtyLabel = (slug) => `swarm:${slug}`;
3490
3698
 
3699
+ // server/deskSetup.ts
3700
+ async function setUpDesk(term, steps) {
3701
+ await term?.releaseIdle?.();
3702
+ await steps.release();
3703
+ return steps.prepare();
3704
+ }
3705
+
3491
3706
  // server/fixOutcome.ts
3492
3707
  var MAX_FIX_FAILURES = 2;
3493
3708
  function fixOutcome(rec, head, minutes, counts = true) {
@@ -3502,7 +3717,7 @@ function fixOutcome(rec, head, minutes, counts = true) {
3502
3717
  pushed: false
3503
3718
  };
3504
3719
  }
3505
- if (rec.fixReason === "checks" || rec.fixReason === "conflict") {
3720
+ if (rec.fixReason === "checks" || rec.fixReason === "conflict" && rec.passedSha) {
3506
3721
  return { set: { status: "passed", sessionFailures: 0, mergeNote: "waiting for fresh checks" }, log: { kind: "done", text: `\u2714 PR #${pr} fixed in ${minutes}m. Back in line to merge.` }, pushed: true };
3507
3722
  }
3508
3723
  return { set: { status: "queued", round: rec.round + 1, sessionFailures: 0 }, log: { kind: "done", text: `\u2714 Fix pushed for PR #${pr} in ${minutes}m. Back to QA.` }, pushed: true };
@@ -3517,6 +3732,25 @@ var HttpError = class extends Error {
3517
3732
  status;
3518
3733
  };
3519
3734
 
3735
+ // server/issueClaims.ts
3736
+ var RECENT_MERGE_MS = 6 * 60 * 6e4;
3737
+ function branchIssue(headRefName) {
3738
+ const m = /^swarm\/issue-(\d+)-/.exec(headRefName);
3739
+ return m ? Number(m[1]) : null;
3740
+ }
3741
+ function issuesResolvedBy(pr) {
3742
+ const n = branchIssue(pr.headRefName);
3743
+ return [...new Set(n == null ? pr.closesIssues : [...pr.closesIssues, n])];
3744
+ }
3745
+ function issueTaken(n, agents, pulls, now2) {
3746
+ if (agents.some((a) => a.role === "dev" && a.task !== "qa" && a.issueNumber === n && a.status !== "idle" && a.status !== "error")) return true;
3747
+ return pulls.some((p) => {
3748
+ if (p.state === "CLOSED") return false;
3749
+ if (p.state === "MERGED" && (!p.mergedAt || now2 - Date.parse(p.mergedAt) > RECENT_MERGE_MS)) return false;
3750
+ return issuesResolvedBy(p).includes(n);
3751
+ });
3752
+ }
3753
+
3520
3754
  // server/mergeGate.ts
3521
3755
  var MAX_MERGE_FIXES = 3;
3522
3756
  var CHECKS_ALERT_MS = 30 * 6e4;
@@ -3556,6 +3790,57 @@ function orphanedQa(qa, agents, busy) {
3556
3790
  );
3557
3791
  }
3558
3792
 
3793
+ // server/qaOutcome.ts
3794
+ var MAX_QA_ROUNDS = 3;
3795
+ function lastQaRound(rec) {
3796
+ return rec.round - rec.retests >= MAX_QA_ROUNDS;
3797
+ }
3798
+ function qaOutcome(pass, rec, pr) {
3799
+ if (pass) return "passed";
3800
+ if (!lastQaRound(rec)) return "failed";
3801
+ const conflicting = pr != null && (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY");
3802
+ return conflicting && rec.mergeFixes < MAX_MERGE_FIXES ? "conflict" : "needs-human";
3803
+ }
3804
+ function conflictFixInstructions(qaInstructions2, base) {
3805
+ const merge = `It also conflicts with ${base}: bring it up to date (git fetch origin && git merge origin/${base}) and resolve the conflicts so both this change and the newly merged work keep working.`;
3806
+ return qaInstructions2.trim() ? `${qaInstructions2.trim()}
3807
+
3808
+ ${merge}` : merge;
3809
+ }
3810
+
3811
+ // server/qaPrompt.ts
3812
+ function qaInstructions(i) {
3813
+ return { checks: checksLine(i), testStep: testStep(i.checks !== "none"), retest: retest(i) };
3814
+ }
3815
+ function checksLine(i) {
3816
+ const names = (list) => list.join(", ") || "unnamed";
3817
+ const state = i.checks === "passing" ? `passing (${names(i.checkNames)})` : i.checks === "failing" ? `failing: ${names(i.failedChecks)}${i.pendingChecks.length ? `; still running: ${i.pendingChecks.join(", ")}` : ""}` : i.checks === "pending" ? `pending: ${names(i.pendingChecks)}` : "none";
3818
+ return `GitHub checks right now: ${state}`;
3819
+ }
3820
+ function testStep(hasChecks) {
3821
+ return hasChecks ? "3. GitHub's checks run on this PR, and it merges only once they're green. Read .github/workflows to see what they cover and don't re-run that locally. Run only what you need to exercise the change (for example a build to start the app), anything the checks don't cover, or a failing check to reproduce it. A failing check is a finding: name it." : "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).";
3822
+ }
3823
+ function retest(i) {
3824
+ const since = i.lastTestedSha && i.lastTestedSha !== i.headSha ? i.lastTestedSha : null;
3825
+ const diff = since ? `(git diff ${since}..HEAD), plus a quick smoke test of the main flow. Don't repeat the full test plan for parts the change didn't touch.` : null;
3826
+ if (i.fixReason === "conflict") {
3827
+ return `
3828
+ QA passed it before, but since then the branch was updated with ${i.defaultBranch} to resolve merge conflicts. Re-check everything, especially where this change meets the newly merged work.`;
3829
+ }
3830
+ if (i.fixReason === "checks") {
3831
+ return `
3832
+ QA passed it before, but since then the developer changed the code to fix failing GitHub checks. ${diff ? `Test what changed since QA passed it ${diff}` : "Re-check everything."}`;
3833
+ }
3834
+ if (i.round > 1 && i.summary) {
3835
+ return `
3836
+ This is a re-test after fixes. Last round's findings:
3837
+ ${i.summary}
3838
+ ${i.fixInstructions ?? ""}
3839
+ Check those first, then ${diff ? `test what changed since the last round ${diff}` : "re-check everything else."}`;
3840
+ }
3841
+ return "";
3842
+ }
3843
+
3559
3844
  // server/restartRecovery.ts
3560
3845
  function followKeptCli(a, cli) {
3561
3846
  if (!cli?.busy || a.status === "preparing") return false;
@@ -3724,6 +4009,7 @@ var Previews = class {
3724
4009
  }
3725
4010
  /** The floor is leaving the building: stop its preview and remove the preview worktree. */
3726
4011
  async remove(f) {
4012
+ const main = this.backend.mainDir(f.fullName);
3727
4013
  const r = this.runs.get(f.id);
3728
4014
  if (r) {
3729
4015
  r.gen++;
@@ -3732,7 +4018,7 @@ var Previews = class {
3732
4018
  this.runs.delete(f.id);
3733
4019
  this.hasDefault.delete(f.id);
3734
4020
  clearTimeout(this.emitTimers.get(f.id));
3735
- await this.backend.removeDesk(f.fullName, PREVIEW_SLUG).catch(() => void 0);
4021
+ await this.backend.removeDesk(f.fullName, PREVIEW_SLUG, main).catch(() => void 0);
3736
4022
  }
3737
4023
  /** Server shutdown: stop every running preview. */
3738
4024
  async stopAll(floors) {
@@ -3777,6 +4063,96 @@ var Previews = class {
3777
4063
  }
3778
4064
  };
3779
4065
 
4066
+ // server/prompts.ts
4067
+ var PLACEHOLDERS = {
4068
+ worktree: "<your worktree>",
4069
+ issue: "<issue>",
4070
+ pr: "<PR>",
4071
+ prTitle: "<PR title>",
4072
+ prUrl: "<PR URL>",
4073
+ branch: "<branch>"
4074
+ };
4075
+ var devBranch = (issue2, slug) => `swarm/issue-${issue2}-${slug}`;
4076
+ var qaBranch = (pr, slug) => `qa/pr-${pr}-${slug}`;
4077
+ function devSystemPrompt({ agent: a, repo, port, cwd, branch, linked, fixing }) {
4078
+ const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
4079
+ const links = linked.map((r) => `- ${r.fullName}: read-only reference clone at ${r.dir}`);
4080
+ return [
4081
+ `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.`,
4082
+ `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.`,
4083
+ a.brief ? `
4084
+ Your job description:
4085
+ ${a.brief}` : "",
4086
+ "",
4087
+ `Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
4088
+ repo.summary ? `Project: ${repo.summary}` : "",
4089
+ repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
4090
+ `Your worktree: ${cwd}`,
4091
+ 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})`,
4092
+ links.length ? `Related repositories you may read for context (do not modify them):
4093
+ ${links.join("\n")}` : "",
4094
+ "",
4095
+ "Workflow:",
4096
+ "1. Read the issue and explore the relevant code before changing anything.",
4097
+ "2. Implement the change with focused commits and clear messages.",
4098
+ "3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed.",
4099
+ 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.",
4100
+ `5. Push: ${push}`,
4101
+ 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>".`,
4102
+ fixing ? "" : "7. End your final message with the pull request URL on its own line.",
4103
+ "",
4104
+ "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."
4105
+ ].filter((l) => l !== "").join("\n");
4106
+ }
4107
+ function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, testStep: testStep2 }) {
4108
+ return [
4109
+ `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.`,
4110
+ ...a.brief ? ["", `Your job description:
4111
+ ${a.brief}`] : [],
4112
+ ...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."] : [],
4113
+ "",
4114
+ `Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
4115
+ ...repo.summary ? [`Project: ${repo.summary}`] : [],
4116
+ ...repo.mission ? [`What the team is building (the manager's brief): ${repo.mission}`] : [],
4117
+ ...repo.qaBrief ? [`What to check on this project (from the CEO):
4118
+ ${repo.qaBrief}`] : [],
4119
+ `Pull request #${pr.number} "${pr.title}" from branch ${pr.headRefName}: ${pr.url}`,
4120
+ `Your worktree: ${cwd}. It has the pull request's code checked out on local branch ${branch}.`,
4121
+ "",
4122
+ "How to test:",
4123
+ "1. Read the PR description and the linked issue, and work out the acceptance criteria.",
4124
+ `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.`,
4125
+ testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
4126
+ 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).",
4127
+ "5. You may write throwaway scripts to probe behaviour, but do not commit them.",
4128
+ "A merge conflict with the default branch is not a fail: judge the change itself (on a throwaway merge if you need newer work) and pass it if it's good. The office sends conflicting PRs back for a merge fix, then you re-test.",
4129
+ "",
4130
+ "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."
4131
+ ].join("\n");
4132
+ }
4133
+ var OFFICE = "Office instructions";
4134
+ var MARK = "\0job description\0";
4135
+ function promptParts(build, brief) {
4136
+ if (!brief) return [{ label: OFFICE, text: build(""), editable: false }];
4137
+ const [before, after] = build(MARK).split(MARK);
4138
+ return [
4139
+ { label: OFFICE, text: before, editable: false },
4140
+ { label: "Job description", text: brief, editable: true },
4141
+ { label: OFFICE, text: after, editable: false }
4142
+ ].filter((p) => p.text !== "");
4143
+ }
4144
+ var view = (kind, parts) => ({ kind, text: parts.map((p) => p.text).join(""), parts });
4145
+ function devPromptPreview({ slug, ...i }) {
4146
+ const branch = devBranch(PLACEHOLDERS.issue, slug);
4147
+ return view("dev", promptParts((brief) => devSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch }), i.agent.brief));
4148
+ }
4149
+ function qaPromptPreview({ slug, ...i }) {
4150
+ const pr = { number: PLACEHOLDERS.pr, title: PLACEHOLDERS.prTitle, headRefName: PLACEHOLDERS.branch, url: PLACEHOLDERS.prUrl };
4151
+ const branch = qaBranch(PLACEHOLDERS.pr, slug);
4152
+ return view("qa", promptParts((brief) => qaSystemPrompt({ ...i, agent: { ...i.agent, brief }, cwd: PLACEHOLDERS.worktree, branch, pr }), i.agent.brief));
4153
+ }
4154
+ var ceoPromptPreview = (text) => view("ceo", [{ label: OFFICE, text, editable: false }]);
4155
+
3780
4156
  // server/pacing.ts
3781
4157
  var PACING_MS = 60 * 6e4;
3782
4158
  var DEFAULT_PACING_SESSIONS = 3;
@@ -3835,7 +4211,10 @@ var AgentTerminal = class {
3835
4211
  rows = TERM_ROWS;
3836
4212
  /** Output arrived since the last save. */
3837
4213
  dirty = false;
3838
- /** Closes a CLI left waiting at its prompt after its task (set by the runtime that left it there). */
4214
+ /**
4215
+ * Closes a CLI left waiting at its prompt after its task, settling once it has exited (set by the runtime that left
4216
+ * it there; null while a CLI drives a session).
4217
+ */
3839
4218
  releaseIdle = null;
3840
4219
  /** Something the manager typed at that waiting prompt: the office takes it on as a follow-up (true) or refuses. */
3841
4220
  onIdlePrompt = null;
@@ -4046,7 +4425,6 @@ var FEMININE_NAMES = new Set(
4046
4425
  var lookFor = (name) => FEMININE_NAMES.has(name.trim().split(/\s+/)[0].toLowerCase()) ? "feminine" : "masculine";
4047
4426
  var LOOKS = ["feminine", "masculine"];
4048
4427
  var MAX_DESKS = { ...FLOOR_DESKS, ceo: 1 };
4049
- var MAX_QA_ROUNDS = 3;
4050
4428
  var DEFAULT_MODEL = "claude-opus-5-5";
4051
4429
  var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
4052
4430
  var CEO_MODEL = "claude-opus-5-5";
@@ -4211,6 +4589,8 @@ var Swarm = class {
4211
4589
  messageSeq = 1;
4212
4590
  agentRt = /* @__PURE__ */ new Map();
4213
4591
  repoRt = /* @__PURE__ */ new Map();
4592
+ deskAlerts = /* @__PURE__ */ new Map();
4593
+ // agent id -> the desk setup error the manager was last told about
4214
4594
  clients = /* @__PURE__ */ new Set();
4215
4595
  user = null;
4216
4596
  ghError;
@@ -4363,6 +4743,7 @@ var Swarm = class {
4363
4743
  setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
4364
4744
  setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
4365
4745
  setInterval(() => void this.saveTerminals(), TERMINAL_SAVE_MS);
4746
+ setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.sweepFloor(r.id), i * 1500)), DESK_SWEEP_INTERVAL_MS);
4366
4747
  void this.backend.detectClis().then((clis) => {
4367
4748
  this.clis = clis;
4368
4749
  this.broadcast({ type: "clis", clis });
@@ -4805,8 +5186,29 @@ var Swarm = class {
4805
5186
  return this.mergeNote(rec, `merge blocked: ${error}`);
4806
5187
  }
4807
5188
  this.toast("success", `\u{1F500} Merged PR #${pr.number} into ${repo.defaultBranch}: ${pr.title}`);
5189
+ await this.closeResolvedIssues(repo, pr.number, pr);
4808
5190
  return true;
4809
5191
  }
5192
+ /**
5193
+ * After the office merges a PR: close the issues it resolves now rather than waiting for GitHub's "Closes #N" (which
5194
+ * lags, and never comes when the link wasn't recognised), so the scheduler doesn't hand them out again. Never throws.
5195
+ */
5196
+ async closeResolvedIssues(repo, prNumber, pr) {
5197
+ const numbers = issuesResolvedBy(pr);
5198
+ const rt = this.repoRt.get(repo.id);
5199
+ if (rt && rt.issues.some((i) => numbers.includes(i.number))) {
5200
+ rt.issues = rt.issues.filter((i) => !numbers.includes(i.number));
5201
+ this.emitRepo(repo);
5202
+ }
5203
+ for (const n of numbers) {
5204
+ try {
5205
+ if (await this.backend.issueState(repo.fullName, n) !== "OPEN") continue;
5206
+ await this.backend.closeIssue(repo.fullName, n, `Shipped in #${prNumber}`);
5207
+ } catch (err) {
5208
+ console.warn(`could not close ${repo.fullName}#${n} after merging #${prNumber}`, err);
5209
+ }
5210
+ }
5211
+ }
4810
5212
  /** Show where auto-merge stands on a PR's card. Doesn't count as a change to the record. */
4811
5213
  mergeNote(rec, text) {
4812
5214
  if (rec.mergeNote !== text) {
@@ -4849,6 +5251,31 @@ var Swarm = class {
4849
5251
  async syncFolderNow(repoId2) {
4850
5252
  return { folderSync: await this.syncFolder(this.repo(repoId2)) };
4851
5253
  }
5254
+ /** Remove the floor's desks, desk folders and swarm/qa branches nobody uses any more; say so when something went. */
5255
+ async sweepFloor(repoId2) {
5256
+ const repo = this.state.repos.find((r) => r.id === repoId2);
5257
+ const rt = this.repoRt.get(repoId2);
5258
+ if (!repo || !rt || rt.cloneStatus !== "ready") return;
5259
+ try {
5260
+ const pulls = rt.lastSync ? rt.pulls : await this.backend.listPulls(repo.fullName);
5261
+ const agents = this.state.agents.filter((a) => a.repoId === repo.id);
5262
+ const keep = {
5263
+ desks: [...agents.map((a) => this.agentSlug(a)), PREVIEW_SLUG],
5264
+ branches: [...agents.flatMap((a) => a.branch ? [a.branch] : []), ...pulls.filter((p) => p.state === "OPEN").map((p) => p.headRefName)]
5265
+ };
5266
+ const r = await this.backend.sweepDesks(repo.fullName, keep);
5267
+ const desks = r.desks + r.folders;
5268
+ const saved = r.patches.length ? ` (${r.patches.length} patch${r.patches.length === 1 ? "" : "es"} saved to ${path13.dirname(r.patches[0])})` : "";
5269
+ console.log(`desk sweep ${repo.fullName}: ${desks} desks, ${r.branches} branches removed${saved}${r.skipped.length ? `, ${r.skipped.length} still in use` : ""}`);
5270
+ if (!desks && !r.branches) return;
5271
+ const removed = [desks && `${desks} old desk${desks === 1 ? "" : "s"}`, r.branches && `${r.branches} finished branch${r.branches === 1 ? "" : "es"}`].filter(Boolean).join(" and ");
5272
+ const text = `\u{1F9F9} ${repo.fullName.split("/")[1]}: removed ${removed}${saved}`;
5273
+ this.toast("info", text);
5274
+ this.postMessage("office", text);
5275
+ } catch (err) {
5276
+ console.warn(`desk sweep ${repo.fullName} failed: ${oneLine(err)}`);
5277
+ }
5278
+ }
4852
5279
  // ---------- the floor's app (preview monitor) ----------
4853
5280
  /** Run the floor's app from its preview worktree: the default branch, or an open PR. Replaces what it is running now. */
4854
5281
  async startPreview(id, pr) {
@@ -4879,6 +5306,7 @@ var Swarm = class {
4879
5306
  await this.backend.ensureClone(repo.fullName);
4880
5307
  rt.cloneStatus = "ready";
4881
5308
  void this.previews.refreshDefault(repo);
5309
+ void this.sweepFloor(id);
4882
5310
  } catch (err) {
4883
5311
  rt.cloneStatus = "error";
4884
5312
  rt.cloneError = err.message;
@@ -4954,8 +5382,10 @@ var Swarm = class {
4954
5382
  }
4955
5383
  async mergePull(repoId2, number, method = "squash") {
4956
5384
  const repo = this.repo(repoId2);
5385
+ const pr = this.repoRt.get(repo.id)?.pulls.find((p) => p.number === number) ?? await this.backend.prDetails(repo.fullName, number);
4957
5386
  await this.backend.mergePull(repo.fullName, number, method);
4958
5387
  this.toast("success", `Merged PR #${number} into ${repo.defaultBranch}`);
5388
+ await this.closeResolvedIssues(repo, number, pr);
4959
5389
  await this.syncRepo(repo.id);
4960
5390
  setTimeout(() => this.schedule(), 200);
4961
5391
  }
@@ -5072,7 +5502,8 @@ var Swarm = class {
5072
5502
  const repo = this.state.repos.find((r) => r.id === a.repoId);
5073
5503
  if (repo) {
5074
5504
  const slug = this.agentSlug(a);
5075
- void this.backend.releaseDesk(repo.fullName, slug, this.port(a)).then(() => this.backend.removeDesk(repo.fullName, slug)).catch(() => void 0);
5505
+ const main = this.backend.mainDir(repo.fullName);
5506
+ 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
5507
  }
5077
5508
  this.state.agents = this.state.agents.filter((x) => x.id !== id);
5078
5509
  this.agentRt.delete(id);
@@ -5116,9 +5547,12 @@ var Swarm = class {
5116
5547
  this.save();
5117
5548
  }
5118
5549
  issueTaken(repo, n) {
5119
- if (this.state.agents.some((a) => a.repoId === repo.id && a.role === "dev" && a.task !== "qa" && a.issueNumber === n && a.status !== "idle" && a.status !== "error")) return true;
5120
- const pulls = this.repoRt.get(repo.id)?.pulls ?? [];
5121
- return pulls.some((p) => p.state === "OPEN" && (p.closesIssues.includes(n) || p.headRefName.startsWith(`swarm/issue-${n}-`)));
5550
+ return issueTaken(
5551
+ n,
5552
+ this.state.agents.filter((a) => a.repoId === repo.id),
5553
+ this.repoRt.get(repo.id)?.pulls ?? [],
5554
+ Date.now()
5555
+ );
5122
5556
  }
5123
5557
  ensureSlot() {
5124
5558
  if (this.slotsFull()) {
@@ -5197,34 +5631,18 @@ var Swarm = class {
5197
5631
  t.attach(ws);
5198
5632
  }
5199
5633
  buildSystemAppend(a, repo, cwd, branch, fixing) {
5200
- const linked = this.linkedRepos(repo).map((r) => `- ${r.fullName}: read-only reference clone at ${this.backend.mainDir(r.fullName)}`);
5201
- const push = fixing ? `git push origin HEAD:${fixing.headRef}` : `git push -u origin ${branch}`;
5202
- return [
5203
- `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.`,
5204
- `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.`,
5205
- a.brief ? `
5206
- Your job description:
5207
- ${a.brief}` : "",
5208
- "",
5209
- `Repository: ${repo.fullName} (default branch: ${repo.defaultBranch})`,
5210
- repo.summary ? `Project: ${repo.summary}` : "",
5211
- repo.mission ? `What the team is building (the manager's brief): ${repo.mission}` : "",
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");
5634
+ return devSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, linked: this.linkedDirs(repo), fixing });
5635
+ }
5636
+ linkedDirs(repo) {
5637
+ return this.linkedRepos(repo).map((r) => ({ fullName: r.fullName, dir: this.backend.mainDir(r.fullName) }));
5638
+ }
5639
+ /** What an agent is told on a task, previewed with placeholders for the task's details. */
5640
+ agentPrompt(id) {
5641
+ const a = this.agent(id);
5642
+ if (a.role === "ceo") return ceoPromptPreview(ceoSystemPrompt(this.ceoPromptInput(a)));
5643
+ const repo = this.repo(a.repoId);
5644
+ const base = { agent: a, repo, port: this.port(a), slug: slugify(a.name) };
5645
+ return a.role === "qa" ? qaPromptPreview(base) : devPromptPreview({ ...base, linked: this.linkedDirs(repo) });
5228
5646
  }
5229
5647
  beginTask(a, patch, banner, preparing) {
5230
5648
  const rt = this.agentRt.get(a.id);
@@ -5252,8 +5670,13 @@ ${linked.join("\n")}` : "",
5252
5670
  async prepare(a, repo, base, branch) {
5253
5671
  try {
5254
5672
  if (this.repoRt.get(repo.id)?.cloneStatus !== "ready") await this.cloneRepo(repo.id);
5255
- await this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a));
5256
- const cwd = await this.backend.prepareDesk(repo.fullName, { defaultBranch: repo.defaultBranch, pr: base.pr }, this.agentSlug(a), branch);
5673
+ const slug = this.agentSlug(a);
5674
+ const note = (text) => this.appendLog(a, [{ kind: "system", text }]);
5675
+ const cwd = await setUpDesk(this.agentRt.get(a.id)?.terminal, {
5676
+ release: () => this.backend.releaseDesk(repo.fullName, slug, this.port(a)),
5677
+ prepare: () => this.backend.prepareDesk(repo.fullName, { defaultBranch: repo.defaultBranch, pr: base.pr }, slug, branch, note)
5678
+ });
5679
+ this.deskAlerts.delete(a.id);
5257
5680
  return a.status === "preparing" ? cwd : null;
5258
5681
  } catch (err) {
5259
5682
  if (a.status !== "preparing") return null;
@@ -5261,13 +5684,17 @@ ${linked.join("\n")}` : "",
5261
5684
  a.endedAt = Date.now();
5262
5685
  a.lastError = err.message;
5263
5686
  this.appendLog(a, [{ kind: "error", text: `\u2717 ${a.lastError}` }]);
5687
+ if (this.deskAlerts.get(a.id) !== a.lastError) {
5688
+ this.deskAlerts.set(a.id, a.lastError);
5689
+ this.postMessage("office", `\u26A0\uFE0F ${a.name}'s desk couldn't be set up: ${a.lastError.slice(0, 240)}`);
5690
+ }
5264
5691
  this.emitAgent(a);
5265
5692
  this.save();
5266
5693
  return null;
5267
5694
  }
5268
5695
  }
5269
5696
  async runTask(a, repo, issue2, note) {
5270
- const branch = `swarm/issue-${issue2.number}-${slugify(a.name)}`;
5697
+ const branch = devBranch(issue2.number, slugify(a.name));
5271
5698
  this.beginTask(
5272
5699
  a,
5273
5700
  { task: "issue", issueNumber: issue2.number, issueTitle: issue2.title, branch, prNumber: null, prUrl: null, sessionId: null },
@@ -5468,33 +5895,11 @@ Note from the manager: ${note}` : ""
5468
5895
  this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
5469
5896
  this.toast("info", `PR #${prNumber} is queued for QA`);
5470
5897
  }
5471
- 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");
5898
+ buildQaSystemAppend(a, repo, cwd, branch, pr, testStep2) {
5899
+ return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr, testStep: testStep2 });
5495
5900
  }
5496
5901
  async runQa(a, repo, rec) {
5497
- const branch = `qa/pr-${rec.prNumber}-${slugify(a.name)}`;
5902
+ const branch = qaBranch(rec.prNumber, slugify(a.name));
5498
5903
  this.setQa(rec, { status: "testing", qaAgentId: a.id });
5499
5904
  this.beginTask(
5500
5905
  a,
@@ -5504,6 +5909,7 @@ ${repo.qaBrief}`] : [],
5504
5909
  );
5505
5910
  let pr;
5506
5911
  let issue2 = null;
5912
+ const lastTestedSha = rec.testedSha;
5507
5913
  try {
5508
5914
  pr = await this.backend.prDetails(repo.fullName, rec.prNumber);
5509
5915
  const issueNumber = rec.issueNumber ?? pr.closesIssues[0] ?? null;
@@ -5533,16 +5939,22 @@ ${repo.qaBrief}`] : [],
5533
5939
  return;
5534
5940
  }
5535
5941
  const dev = rec.devAgentId ? this.state.agents.find((x) => x.id === rec.devAgentId) : null;
5942
+ const failedConflict = rec.fixReason === "conflict" && !rec.passedSha;
5943
+ const qa = qaInstructions({
5944
+ ...pr,
5945
+ round: rec.round,
5946
+ fixReason: failedConflict ? "qa" : rec.fixReason,
5947
+ lastTestedSha: failedConflict ? null : lastTestedSha,
5948
+ summary: rec.summary,
5949
+ fixInstructions: rec.fixInstructions,
5950
+ defaultBranch: repo.defaultBranch
5951
+ });
5536
5952
  const prompt = [
5537
5953
  `Please QA pull request #${pr.number}: ${pr.title}`,
5538
5954
  `URL: ${pr.url}`,
5539
5955
  `Author: ${dev ? `${dev.name} (developer agent)` : "a teammate"} \xB7 QA round ${rec.round}`,
5540
- rec.fixReason === "conflict" ? `
5541
- QA passed it before, but since then the branch was updated with ${repo.defaultBranch} to resolve merge conflicts. Re-check everything, especially where this change meets the newly merged work.` : rec.fixReason === "checks" ? "\nQA passed it before, but since then the developer changed the code to fix failing GitHub checks. Re-check everything." : rec.round > 1 && rec.summary ? `
5542
- This is a re-test after fixes. Last round's findings:
5543
- ${rec.summary}
5544
- ${rec.fixInstructions ?? ""}
5545
- Check those first, then re-check everything else.` : "",
5956
+ qa.checks,
5957
+ qa.retest,
5546
5958
  "",
5547
5959
  "PR description:",
5548
5960
  pr.body.trim() || "(empty)",
@@ -5550,7 +5962,7 @@ Check those first, then re-check everything else.` : "",
5550
5962
  Linked issue: ${issue2.title}
5551
5963
  ${issue2.body.trim() || "(no description)"}` : ""
5552
5964
  ].filter((l) => l !== "").join("\n");
5553
- this.startAgentSession(a, repo, cwd, prompt, this.buildQaSystemAppend(a, repo, cwd, branch, pr), void 0, QA_SCHEMA);
5965
+ this.startAgentSession(a, repo, cwd, prompt, this.buildQaSystemAppend(a, repo, cwd, branch, pr, qa.testStep), void 0, QA_SCHEMA);
5554
5966
  }
5555
5967
  async onQaFinished(a, repo, result) {
5556
5968
  const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
@@ -5574,22 +5986,25 @@ ${issue2.body.trim() || "(no description)"}` : ""
5574
5986
  this.appendLog(a, [{ kind: pass ? "done" : "error", text: `${pass ? "\u2705 QA passed" : "\u274C QA failed"} PR #${a.prNumber} \xB7 ${report.checks.length} checks \xB7 ${rt.shots.length} screenshots` }]);
5575
5987
  let commentUrl = null;
5576
5988
  if (rec) {
5989
+ const pull = !pass && lastQaRound(rec) ? await this.backend.prDetails(repo.fullName, rec.prNumber).catch(() => null) : null;
5990
+ const next = qaOutcome(pass, rec, pull);
5577
5991
  try {
5578
5992
  this.appendLog(a, [{ kind: "system", text: "\u{1F4CE} Uploading evidence and posting the QA report on the PR\u2026" }]);
5579
- const body = await this.renderQaComment(a, repo, rec, report, rt.shots);
5993
+ const body = await this.renderQaComment(a, repo, rec, report, rt.shots, next);
5580
5994
  commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber, body) || null;
5581
5995
  this.appendLog(a, [{ kind: "system", text: ` \u23BF ${commentUrl ?? "comment posted"}` }]);
5582
5996
  } catch (err) {
5583
5997
  this.appendLog(a, [{ kind: "error", text: ` \u23BF Could not post the QA report: ${err.message}` }]);
5584
5998
  }
5585
5999
  const qaRounds = rec.round - rec.retests;
5586
- const nextStatus = pass ? "passed" : qaRounds >= MAX_QA_ROUNDS ? "needs-human" : "failed";
6000
+ const nextStatus = next === "conflict" ? "failed" : next;
6001
+ const fixInstructions = report.fixInstructions ?? report.checks.filter((c) => c.result === "fail").map((c) => `${c.name}: ${c.details}`).join("\n");
5587
6002
  this.setQa(rec, {
5588
6003
  status: nextStatus,
5589
6004
  summary: report.summary,
5590
6005
  checks: report.checks,
5591
6006
  commentUrl: commentUrl ?? rec.commentUrl,
5592
- fixInstructions: report.fixInstructions ?? report.checks.filter((c) => c.result === "fail").map((c) => `${c.name}: ${c.details}`).join("\n"),
6007
+ fixInstructions,
5593
6008
  sessionFailures: 0,
5594
6009
  fixReason: pass ? null : "qa",
5595
6010
  passedSha: pass ? rec.testedSha : null,
@@ -5598,13 +6013,14 @@ ${issue2.body.trim() || "(no description)"}` : ""
5598
6013
  mergeRetryAt: null,
5599
6014
  alerted: false
5600
6015
  });
6016
+ if (next === "conflict") this.sendBack(repo, rec, "conflict", conflictFixInstructions(fixInstructions, repo.defaultBranch), false);
5601
6017
  this.toast(
5602
6018
  pass ? "success" : "error",
5603
- pass ? `\u2705 ${a.name} passed PR #${rec.prNumber}${repo.autoMerge ? "; it merges once GitHub's checks are green" : ": ready to merge"}` : nextStatus === "needs-human" ? `\u274C PR #${rec.prNumber} failed QA ${qaRounds} times and needs a human` : `\u274C ${a.name} failed PR #${rec.prNumber}; sending it back to the developer`
6019
+ pass ? `\u2705 ${a.name} passed PR #${rec.prNumber}${repo.autoMerge ? "; it merges once GitHub's checks are green" : ": ready to merge"}` : nextStatus === "needs-human" ? `\u274C PR #${rec.prNumber} failed QA ${qaRounds} times and needs a human` : next === "conflict" ? `\u274C ${a.name} failed PR #${rec.prNumber}, and it conflicts with ${repo.defaultBranch}; sending it back to merge and fix` : `\u274C ${a.name} failed PR #${rec.prNumber}; sending it back to the developer`
5604
6020
  );
5605
6021
  }
5606
6022
  }
5607
- async renderQaComment(a, repo, rec, report, shots) {
6023
+ async renderQaComment(a, repo, rec, report, shots, next) {
5608
6024
  const pass = report.verdict === "pass";
5609
6025
  const images = [];
5610
6026
  const evidence = shots.slice(-8);
@@ -5642,7 +6058,7 @@ ${issue2.body.trim() || "(no description)"}` : ""
5642
6058
  if (images.length) lines.push("", "### \u{1F4F8} Evidence", "", ...images.flatMap((img) => [img, ""]));
5643
6059
  else lines.push("", "_No browser screenshots were taken in this round._");
5644
6060
  const merge = repo.autoMerge ? "merges automatically once GitHub's checks pass" : "ready for the manager to merge";
5645
- lines.push("", `<sub>Posted by cubefarm \xB7 ${pass ? merge : rec.round - rec.retests >= MAX_QA_ROUNDS ? "needs a human decision" : "sent back to the developer for fixes"}</sub>`);
6061
+ lines.push("", `<sub>Posted by cubefarm \xB7 ${pass ? merge : next === "needs-human" ? "needs a human decision" : next === "conflict" ? `sent back to the developer to merge ${repo.defaultBranch} and fix` : "sent back to the developer for fixes"}</sub>`);
5646
6062
  return lines.join("\n");
5647
6063
  }
5648
6064
  /** Send a failed PR back to the developer who wrote it (or any free developer on the floor). */
@@ -5667,7 +6083,7 @@ ${issue2.body.trim() || "(no description)"}` : ""
5667
6083
  const failed2 = rec.checks.filter((c) => c.result === "fail");
5668
6084
  const takeover = original ? "" : " A teammate wrote it, so read the PR and the linked issue first.";
5669
6085
  const push = `push to the same branch: git push origin HEAD:${headRef}`;
5670
- const mergeFix = rec.fixReason === "conflict" ? [
6086
+ const mergeFix = rec.fixReason === "conflict" && rec.passedSha ? [
5671
6087
  `QA passed pull request #${rec.prNumber} (${pull?.url ?? ""}), but it now conflicts with ${repo.defaultBranch} because other work was merged first.${takeover}`,
5672
6088
  "",
5673
6089
  `Bring it up to date: git fetch origin && git merge origin/${repo.defaultBranch}. Resolve the conflicts so both this change and the newly merged work keep working, run the project's checks, and ${push}`
@@ -6010,11 +6426,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
6010
6426
  }
6011
6427
  emitOfficeUpdate() {
6012
6428
  if (!this.officeHead) return;
6013
- const view = this.officeUpdateView();
6014
- const key = JSON.stringify(view);
6429
+ const view2 = this.officeUpdateView();
6430
+ const key = JSON.stringify(view2);
6015
6431
  if (key === this.lastOfficeView) return;
6016
6432
  this.lastOfficeView = key;
6017
- this.broadcast({ type: "officeUpdate", officeUpdate: view });
6433
+ this.broadcast({ type: "officeUpdate", officeUpdate: view2 });
6018
6434
  }
6019
6435
  /** The own-folder check found the office this many commits behind GitHub. */
6020
6436
  setOfficeBehind(behind) {
@@ -6168,11 +6584,11 @@ Full report with screenshots: ${rec.commentUrl}` : "",
6168
6584
  }
6169
6585
  ceoInfo() {
6170
6586
  const c = this.state.ceo;
6171
- const view = (j) => ({ kind: j.kind, label: jobLabel(j, this.ceoFloor(j.repoId)) });
6587
+ const view2 = (j) => ({ kind: j.kind, label: jobLabel(j, this.ceoFloor(j.repoId)) });
6172
6588
  const min = this.state.settings.ceoHeartbeatMin;
6173
6589
  return {
6174
- queue: c.queue.map(view),
6175
- job: c.job ? view(c.job) : null,
6590
+ queue: c.queue.map(view2),
6591
+ job: c.job ? view2(c.job) : null,
6176
6592
  lastReviewAt: c.lastReviewAt,
6177
6593
  nextReviewAt: min > 0 && c.lastReviewAt ? c.lastReviewAt + min * 6e4 : null
6178
6594
  };
@@ -6216,6 +6632,10 @@ ${job.text}` } : job;
6216
6632
  }
6217
6633
  void this.runCeoJob(a, job);
6218
6634
  }
6635
+ ceoPromptInput(a) {
6636
+ const s = this.state.settings;
6637
+ 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 };
6638
+ }
6219
6639
  async runCeoJob(a, job) {
6220
6640
  const rt = this.agentRt.get(a.id);
6221
6641
  const floor = this.ceoFloor(job.repoId);
@@ -6241,21 +6661,12 @@ ${job.text}` } : job;
6241
6661
  this.emitCeo();
6242
6662
  return;
6243
6663
  }
6244
- const s = this.state.settings;
6245
6664
  const how = this.sessionRuntime(a, job.kind === "chat" ? a.sessionId ?? void 0 : void 0);
6246
6665
  rt.session = this.backend.startSession(
6247
6666
  {
6248
6667
  cwd: CEO_DIR,
6249
6668
  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
- }),
6669
+ systemAppend: ceoSystemPrompt(this.ceoPromptInput(a)),
6259
6670
  model: a.model || CEO_MODEL,
6260
6671
  effort: a.effort || CEO_EFFORT,
6261
6672
  browserTesting: false,
@@ -6848,6 +7259,7 @@ app.post("/api/agents/:id/assign", route((req) => swarm.assign(String(req.params
6848
7259
  app.post("/api/agents/:id/stop", route((req) => swarm.stopAgent(String(req.params.id))));
6849
7260
  app.post("/api/agents/:id/reset", route((req) => swarm.resetAgent(String(req.params.id))));
6850
7261
  app.post("/api/agents/:id/message", route((req) => swarm.message(String(req.params.id), str(req.body.text))));
7262
+ app.get("/api/agents/:id/prompt", route((req) => swarm.agentPrompt(String(req.params.id))));
6851
7263
  app.get("/api/agents/:id/screen", (req, res) => {
6852
7264
  const shot = swarm.screenshot(String(req.params.id));
6853
7265
  if (!shot) return void res.status(404).end();