cubefarm 0.3.8 → 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.
- package/dist/assets/ToyWorld-CCa77hRS.js +2 -0
- package/dist/assets/{index-B7sInPPW.js → index-g6QWWXij.js} +68 -68
- package/dist/assets/{rapier-sDtDRX5B.js → rapier--PAOxOZs.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/index.js +176 -46
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/assets/ToyWorld-DAEljc3S.js +0 -2
package/dist/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-g6QWWXij.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-CIr98Zaa.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
package/dist-server/index.js
CHANGED
|
@@ -194,8 +194,10 @@ async function prForBranch(fullName, branch) {
|
|
|
194
194
|
const list = await ghJson(["pr", "list", "-R", fullName, "--head", branch, "--state", "all", "--json", "number,url", "--limit", "1"]);
|
|
195
195
|
return list[0] ?? null;
|
|
196
196
|
}
|
|
197
|
+
var uniqueNames = (checks) => [...new Set(checks.map(checkName))];
|
|
197
198
|
async function prDetails(fullName, number) {
|
|
198
|
-
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 ?? [];
|
|
199
201
|
return {
|
|
200
202
|
number: raw.number,
|
|
201
203
|
title: raw.title,
|
|
@@ -207,7 +209,11 @@ async function prDetails(fullName, number) {
|
|
|
207
209
|
closesIssues: (raw.closingIssuesReferences ?? []).map((r) => r.number),
|
|
208
210
|
state: raw.state,
|
|
209
211
|
mergeable: raw.mergeable,
|
|
210
|
-
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))
|
|
211
217
|
};
|
|
212
218
|
}
|
|
213
219
|
async function issueDetails(fullName, number) {
|
|
@@ -457,7 +463,32 @@ async function createProject(root, name, opts) {
|
|
|
457
463
|
async function removeDir(dir) {
|
|
458
464
|
await fs2.rm(dir, { recursive: true, force: true, maxRetries: 8, retryDelay: 250 });
|
|
459
465
|
}
|
|
460
|
-
|
|
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) {
|
|
461
492
|
return withRepoLock(fullName, async () => {
|
|
462
493
|
const main = mainDir(fullName);
|
|
463
494
|
await git(["fetch", "origin", "--prune"], { cwd: main, timeoutMs: 18e4 });
|
|
@@ -476,22 +507,25 @@ function prepareDesk(fullName, base, agentSlug, branch) {
|
|
|
476
507
|
const wt = deskDir(fullName, agentSlug);
|
|
477
508
|
if (await exists(path2.join(wt, ".git"))) {
|
|
478
509
|
try {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
}
|
|
482
519
|
return wt;
|
|
483
|
-
} 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);
|
|
484
524
|
}
|
|
485
525
|
}
|
|
486
526
|
if (await exists(wt)) {
|
|
487
527
|
await git(["worktree", "remove", "--force", wt], { cwd: main }).catch(() => void 0);
|
|
488
|
-
|
|
489
|
-
await removeDir(wt);
|
|
490
|
-
} catch (err) {
|
|
491
|
-
throw new Error(
|
|
492
|
-
`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.`
|
|
493
|
-
);
|
|
494
|
-
}
|
|
528
|
+
await clearDeskFolder(wt);
|
|
495
529
|
}
|
|
496
530
|
await git(["worktree", "prune"], { cwd: main });
|
|
497
531
|
await fs2.mkdir(path2.dirname(wt), { recursive: true });
|
|
@@ -1702,12 +1736,13 @@ function endLive(live) {
|
|
|
1702
1736
|
routes.delete(live.token);
|
|
1703
1737
|
live.term.releaseIdle = null;
|
|
1704
1738
|
live.term.bind(null);
|
|
1705
|
-
const gone = live.proc ? exitOf(live.proc) :
|
|
1739
|
+
const gone = live.proc ? exitOf(live.proc) : Promise.resolve();
|
|
1706
1740
|
live.proc?.kill();
|
|
1707
1741
|
live.proc = null;
|
|
1708
1742
|
live.term.note(`\u2500\u2500 ${cliLabel(live.cli)} session ended \u2500\u2500`);
|
|
1709
1743
|
fs7.rm(live.dir, { recursive: true, force: true, maxRetries: 5 }, () => void 0);
|
|
1710
|
-
|
|
1744
|
+
if (live.cli === "codex" && live.resumeId) void codexThread("archive", live.resumeId, gone);
|
|
1745
|
+
return gone;
|
|
1711
1746
|
}
|
|
1712
1747
|
var metaOf = (l) => ({ agentId: l.agentId, cli: l.cli, token: l.token, dir: l.dir, resumeId: l.resumeId, busy: !!l.session });
|
|
1713
1748
|
var remember = (l) => l.proc?.setMeta(metaOf(l));
|
|
@@ -2551,18 +2586,25 @@ function devScript(opts, cb, issueNumber, issueTitle) {
|
|
|
2551
2586
|
[{ kind: "tool", tool: "Bash", text: `\u23FA $ gh pr create --title "${issueTitle}" --body "Closes #${issueNumber}"` }]
|
|
2552
2587
|
];
|
|
2553
2588
|
}
|
|
2554
|
-
function qaScript(cb, pr, title, round) {
|
|
2589
|
+
function qaScript(cb, pr, title, round, checks) {
|
|
2555
2590
|
const port = 5600 + pr % 50;
|
|
2556
2591
|
const hue = pr * 41 % 360;
|
|
2557
|
-
|
|
2558
|
-
[{ kind: "text", text: `\u25CF Testing PR #${pr} "${title}" (round ${round}). First, the acceptance criteria from the issue.` }],
|
|
2559
|
-
[{ 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") ? [
|
|
2560
2593
|
[{ kind: "tool", tool: "Bash", text: "\u23FA $ npm ci && npm test -- --run" }],
|
|
2561
2594
|
[
|
|
2562
2595
|
{ kind: "result", text: " \u23BF Test Files 8 passed (8)" },
|
|
2563
2596
|
{ kind: "result", text: " Tests 41 passed (41)" }
|
|
2564
2597
|
],
|
|
2565
|
-
[{ 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,
|
|
2566
2608
|
[{ kind: "tool", tool: "Bash", text: `\u23FA $ npm run preview -- --port ${port} &` }, { kind: "result", text: ` \u23BF Local: http://localhost:${port}/` }],
|
|
2567
2609
|
() => cb.browserUrl(`http://localhost:${port}/`),
|
|
2568
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}/` }],
|
|
@@ -2598,7 +2640,8 @@ function fakeSession(opts, cb, fullName) {
|
|
|
2598
2640
|
{ kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort` },
|
|
2599
2641
|
{ kind: "system", text: ` cwd ${opts.cwd}` }
|
|
2600
2642
|
];
|
|
2601
|
-
const
|
|
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);
|
|
2602
2645
|
const script = [header, ...body];
|
|
2603
2646
|
const finish = () => {
|
|
2604
2647
|
const costUsd = 0.3 + Math.random();
|
|
@@ -2887,7 +2930,11 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
|
|
|
2887
2930
|
closesIssues: pr.closesIssues,
|
|
2888
2931
|
state: pr.state,
|
|
2889
2932
|
mergeable: pr.mergeable,
|
|
2890
|
-
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
|
|
2891
2938
|
};
|
|
2892
2939
|
},
|
|
2893
2940
|
issueDetails: async (fullName, number) => {
|
|
@@ -3649,6 +3696,13 @@ function specialtySlug(s) {
|
|
|
3649
3696
|
}
|
|
3650
3697
|
var specialtyLabel = (slug) => `swarm:${slug}`;
|
|
3651
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
|
+
|
|
3652
3706
|
// server/fixOutcome.ts
|
|
3653
3707
|
var MAX_FIX_FAILURES = 2;
|
|
3654
3708
|
function fixOutcome(rec, head, minutes, counts = true) {
|
|
@@ -3663,7 +3717,7 @@ function fixOutcome(rec, head, minutes, counts = true) {
|
|
|
3663
3717
|
pushed: false
|
|
3664
3718
|
};
|
|
3665
3719
|
}
|
|
3666
|
-
if (rec.fixReason === "checks" || rec.fixReason === "conflict") {
|
|
3720
|
+
if (rec.fixReason === "checks" || rec.fixReason === "conflict" && rec.passedSha) {
|
|
3667
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 };
|
|
3668
3722
|
}
|
|
3669
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 };
|
|
@@ -3736,6 +3790,57 @@ function orphanedQa(qa, agents, busy) {
|
|
|
3736
3790
|
);
|
|
3737
3791
|
}
|
|
3738
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
|
+
|
|
3739
3844
|
// server/restartRecovery.ts
|
|
3740
3845
|
function followKeptCli(a, cli) {
|
|
3741
3846
|
if (!cli?.busy || a.status === "preparing") return false;
|
|
@@ -3999,7 +4104,7 @@ ${links.join("\n")}` : "",
|
|
|
3999
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."
|
|
4000
4105
|
].filter((l) => l !== "").join("\n");
|
|
4001
4106
|
}
|
|
4002
|
-
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr }) {
|
|
4107
|
+
function qaSystemPrompt({ agent: a, repo, port, cwd, branch, pr, testStep: testStep2 }) {
|
|
4003
4108
|
return [
|
|
4004
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.`,
|
|
4005
4110
|
...a.brief ? ["", `Your job description:
|
|
@@ -4017,9 +4122,10 @@ ${repo.qaBrief}`] : [],
|
|
|
4017
4122
|
"How to test:",
|
|
4018
4123
|
"1. Read the PR description and the linked issue, and work out the acceptance criteria.",
|
|
4019
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.`,
|
|
4020
|
-
"3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4125
|
+
testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
|
|
4021
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).",
|
|
4022
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.",
|
|
4023
4129
|
"",
|
|
4024
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."
|
|
4025
4131
|
].join("\n");
|
|
@@ -4105,7 +4211,10 @@ var AgentTerminal = class {
|
|
|
4105
4211
|
rows = TERM_ROWS;
|
|
4106
4212
|
/** Output arrived since the last save. */
|
|
4107
4213
|
dirty = false;
|
|
4108
|
-
/**
|
|
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
|
+
*/
|
|
4109
4218
|
releaseIdle = null;
|
|
4110
4219
|
/** Something the manager typed at that waiting prompt: the office takes it on as a follow-up (true) or refuses. */
|
|
4111
4220
|
onIdlePrompt = null;
|
|
@@ -4316,7 +4425,6 @@ var FEMININE_NAMES = new Set(
|
|
|
4316
4425
|
var lookFor = (name) => FEMININE_NAMES.has(name.trim().split(/\s+/)[0].toLowerCase()) ? "feminine" : "masculine";
|
|
4317
4426
|
var LOOKS = ["feminine", "masculine"];
|
|
4318
4427
|
var MAX_DESKS = { ...FLOOR_DESKS, ceo: 1 };
|
|
4319
|
-
var MAX_QA_ROUNDS = 3;
|
|
4320
4428
|
var DEFAULT_MODEL = "claude-opus-5-5";
|
|
4321
4429
|
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
4322
4430
|
var CEO_MODEL = "claude-opus-5-5";
|
|
@@ -4481,6 +4589,8 @@ var Swarm = class {
|
|
|
4481
4589
|
messageSeq = 1;
|
|
4482
4590
|
agentRt = /* @__PURE__ */ new Map();
|
|
4483
4591
|
repoRt = /* @__PURE__ */ new Map();
|
|
4592
|
+
deskAlerts = /* @__PURE__ */ new Map();
|
|
4593
|
+
// agent id -> the desk setup error the manager was last told about
|
|
4484
4594
|
clients = /* @__PURE__ */ new Set();
|
|
4485
4595
|
user = null;
|
|
4486
4596
|
ghError;
|
|
@@ -5560,8 +5670,13 @@ var Swarm = class {
|
|
|
5560
5670
|
async prepare(a, repo, base, branch) {
|
|
5561
5671
|
try {
|
|
5562
5672
|
if (this.repoRt.get(repo.id)?.cloneStatus !== "ready") await this.cloneRepo(repo.id);
|
|
5563
|
-
|
|
5564
|
-
const
|
|
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);
|
|
5565
5680
|
return a.status === "preparing" ? cwd : null;
|
|
5566
5681
|
} catch (err) {
|
|
5567
5682
|
if (a.status !== "preparing") return null;
|
|
@@ -5569,6 +5684,10 @@ var Swarm = class {
|
|
|
5569
5684
|
a.endedAt = Date.now();
|
|
5570
5685
|
a.lastError = err.message;
|
|
5571
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
|
+
}
|
|
5572
5691
|
this.emitAgent(a);
|
|
5573
5692
|
this.save();
|
|
5574
5693
|
return null;
|
|
@@ -5776,8 +5895,8 @@ Note from the manager: ${note}` : ""
|
|
|
5776
5895
|
this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
|
|
5777
5896
|
this.toast("info", `PR #${prNumber} is queued for QA`);
|
|
5778
5897
|
}
|
|
5779
|
-
buildQaSystemAppend(a, repo, cwd, branch, pr) {
|
|
5780
|
-
return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr });
|
|
5898
|
+
buildQaSystemAppend(a, repo, cwd, branch, pr, testStep2) {
|
|
5899
|
+
return qaSystemPrompt({ agent: a, repo, port: this.port(a), cwd, branch, pr, testStep: testStep2 });
|
|
5781
5900
|
}
|
|
5782
5901
|
async runQa(a, repo, rec) {
|
|
5783
5902
|
const branch = qaBranch(rec.prNumber, slugify(a.name));
|
|
@@ -5790,6 +5909,7 @@ Note from the manager: ${note}` : ""
|
|
|
5790
5909
|
);
|
|
5791
5910
|
let pr;
|
|
5792
5911
|
let issue2 = null;
|
|
5912
|
+
const lastTestedSha = rec.testedSha;
|
|
5793
5913
|
try {
|
|
5794
5914
|
pr = await this.backend.prDetails(repo.fullName, rec.prNumber);
|
|
5795
5915
|
const issueNumber = rec.issueNumber ?? pr.closesIssues[0] ?? null;
|
|
@@ -5819,16 +5939,22 @@ Note from the manager: ${note}` : ""
|
|
|
5819
5939
|
return;
|
|
5820
5940
|
}
|
|
5821
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
|
+
});
|
|
5822
5952
|
const prompt = [
|
|
5823
5953
|
`Please QA pull request #${pr.number}: ${pr.title}`,
|
|
5824
5954
|
`URL: ${pr.url}`,
|
|
5825
5955
|
`Author: ${dev ? `${dev.name} (developer agent)` : "a teammate"} \xB7 QA round ${rec.round}`,
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
This is a re-test after fixes. Last round's findings:
|
|
5829
|
-
${rec.summary}
|
|
5830
|
-
${rec.fixInstructions ?? ""}
|
|
5831
|
-
Check those first, then re-check everything else.` : "",
|
|
5956
|
+
qa.checks,
|
|
5957
|
+
qa.retest,
|
|
5832
5958
|
"",
|
|
5833
5959
|
"PR description:",
|
|
5834
5960
|
pr.body.trim() || "(empty)",
|
|
@@ -5836,7 +5962,7 @@ Check those first, then re-check everything else.` : "",
|
|
|
5836
5962
|
Linked issue: ${issue2.title}
|
|
5837
5963
|
${issue2.body.trim() || "(no description)"}` : ""
|
|
5838
5964
|
].filter((l) => l !== "").join("\n");
|
|
5839
|
-
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);
|
|
5840
5966
|
}
|
|
5841
5967
|
async onQaFinished(a, repo, result) {
|
|
5842
5968
|
const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
|
|
@@ -5860,22 +5986,25 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5860
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` }]);
|
|
5861
5987
|
let commentUrl = null;
|
|
5862
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);
|
|
5863
5991
|
try {
|
|
5864
5992
|
this.appendLog(a, [{ kind: "system", text: "\u{1F4CE} Uploading evidence and posting the QA report on the PR\u2026" }]);
|
|
5865
|
-
const body = await this.renderQaComment(a, repo, rec, report, rt.shots);
|
|
5993
|
+
const body = await this.renderQaComment(a, repo, rec, report, rt.shots, next);
|
|
5866
5994
|
commentUrl = await this.backend.commentPull(repo.fullName, rec.prNumber, body) || null;
|
|
5867
5995
|
this.appendLog(a, [{ kind: "system", text: ` \u23BF ${commentUrl ?? "comment posted"}` }]);
|
|
5868
5996
|
} catch (err) {
|
|
5869
5997
|
this.appendLog(a, [{ kind: "error", text: ` \u23BF Could not post the QA report: ${err.message}` }]);
|
|
5870
5998
|
}
|
|
5871
5999
|
const qaRounds = rec.round - rec.retests;
|
|
5872
|
-
const nextStatus =
|
|
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");
|
|
5873
6002
|
this.setQa(rec, {
|
|
5874
6003
|
status: nextStatus,
|
|
5875
6004
|
summary: report.summary,
|
|
5876
6005
|
checks: report.checks,
|
|
5877
6006
|
commentUrl: commentUrl ?? rec.commentUrl,
|
|
5878
|
-
fixInstructions
|
|
6007
|
+
fixInstructions,
|
|
5879
6008
|
sessionFailures: 0,
|
|
5880
6009
|
fixReason: pass ? null : "qa",
|
|
5881
6010
|
passedSha: pass ? rec.testedSha : null,
|
|
@@ -5884,13 +6013,14 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5884
6013
|
mergeRetryAt: null,
|
|
5885
6014
|
alerted: false
|
|
5886
6015
|
});
|
|
6016
|
+
if (next === "conflict") this.sendBack(repo, rec, "conflict", conflictFixInstructions(fixInstructions, repo.defaultBranch), false);
|
|
5887
6017
|
this.toast(
|
|
5888
6018
|
pass ? "success" : "error",
|
|
5889
|
-
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`
|
|
5890
6020
|
);
|
|
5891
6021
|
}
|
|
5892
6022
|
}
|
|
5893
|
-
async renderQaComment(a, repo, rec, report, shots) {
|
|
6023
|
+
async renderQaComment(a, repo, rec, report, shots, next) {
|
|
5894
6024
|
const pass = report.verdict === "pass";
|
|
5895
6025
|
const images = [];
|
|
5896
6026
|
const evidence = shots.slice(-8);
|
|
@@ -5928,7 +6058,7 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5928
6058
|
if (images.length) lines.push("", "### \u{1F4F8} Evidence", "", ...images.flatMap((img) => [img, ""]));
|
|
5929
6059
|
else lines.push("", "_No browser screenshots were taken in this round._");
|
|
5930
6060
|
const merge = repo.autoMerge ? "merges automatically once GitHub's checks pass" : "ready for the manager to merge";
|
|
5931
|
-
lines.push("", `<sub>Posted by cubefarm \xB7 ${pass ? merge :
|
|
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>`);
|
|
5932
6062
|
return lines.join("\n");
|
|
5933
6063
|
}
|
|
5934
6064
|
/** Send a failed PR back to the developer who wrote it (or any free developer on the floor). */
|
|
@@ -5953,7 +6083,7 @@ ${issue2.body.trim() || "(no description)"}` : ""
|
|
|
5953
6083
|
const failed2 = rec.checks.filter((c) => c.result === "fail");
|
|
5954
6084
|
const takeover = original ? "" : " A teammate wrote it, so read the PR and the linked issue first.";
|
|
5955
6085
|
const push = `push to the same branch: git push origin HEAD:${headRef}`;
|
|
5956
|
-
const mergeFix = rec.fixReason === "conflict" ? [
|
|
6086
|
+
const mergeFix = rec.fixReason === "conflict" && rec.passedSha ? [
|
|
5957
6087
|
`QA passed pull request #${rec.prNumber} (${pull?.url ?? ""}), but it now conflicts with ${repo.defaultBranch} because other work was merged first.${takeover}`,
|
|
5958
6088
|
"",
|
|
5959
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}`
|