cubefarm 0.3.4 → 0.3.6

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/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-BCFM-9fU.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-CvwxmawV.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-BU33ZUhQ.css">
13
13
  </head>
14
14
  <body>
@@ -2386,13 +2386,13 @@ function qaScript(cb, pr, title, round) {
2386
2386
  [{ kind: "tool", tool: "mcp__playwright__browser_console_messages", text: "\u23FA \u{1F310} console_messages" }, { kind: "result", text: " \u23BF No errors" }]
2387
2387
  ];
2388
2388
  }
2389
- function fixScript(pr) {
2389
+ function fixScript(pr, pushes) {
2390
2390
  return [
2391
2391
  [{ kind: "text", text: `\u25CF Reading the QA report for PR #${pr}. The toolbar overflows on phones; I'll let it wrap.` }],
2392
2392
  [{ kind: "tool", tool: "Read", text: "\u23FA Read src/styles.css" }, { kind: "result", text: " \u23BF Read 212 lines" }],
2393
2393
  [{ kind: "tool", tool: "Edit", text: "\u23FA Edit src/styles.css" }, { kind: "result", text: " \u23BF Updated" }],
2394
2394
  [{ kind: "tool", tool: "Bash", text: "\u23FA $ npm test -- --run" }, { kind: "result", text: " \u23BF Test Files 8 passed (8)" }],
2395
- [{ kind: "tool", tool: "Bash", text: '\u23FA $ git commit -am "fix: wrap toolbar on narrow screens" && git push origin HEAD' }, { kind: "result", text: " \u23BF pushed" }]
2395
+ pushes ? [{ kind: "tool", tool: "Bash", text: '\u23FA $ git commit -am "fix: wrap toolbar on narrow screens" && git push origin HEAD' }, { kind: "result", text: " \u23BF pushed" }] : [{ kind: "tool", tool: "Bash", text: "\u23FA $ git status --short" }, { kind: "result", text: " \u23BF M src/styles.css" }]
2396
2396
  ];
2397
2397
  }
2398
2398
  function fakeSession(opts, cb, fullName) {
@@ -2404,11 +2404,12 @@ function fakeSession(opts, cb, fullName) {
2404
2404
  const number = Number((kind === "issue" ? issueMatch?.[1] : prMatch?.[1]) ?? 0);
2405
2405
  const title = (kind === "issue" ? issueMatch?.[2] : prMatch?.[2])?.trim() ?? "follow-up";
2406
2406
  const round = Number(opts.prompt.match(/QA round (\d+)/)?.[1] ?? 1);
2407
+ const pushes = kind !== "fix" || !/FAILED|taking over pull request/.test(opts.prompt) || Math.random() > 0.25;
2407
2408
  const header = [
2408
2409
  { kind: "system", text: `\u273B Claude Code (demo) \xB7 ${opts.model} \xB7 ${opts.effort} effort` },
2409
2410
  { kind: "system", text: ` cwd ${opts.cwd}` }
2410
2411
  ];
2411
- const body = kind === "qa" ? qaScript(cb, number, title, round) : kind === "fix" ? fixScript(number) : devScript(opts, cb, number, title);
2412
+ const body = kind === "qa" ? qaScript(cb, number, title, round) : kind === "fix" ? fixScript(number, pushes) : devScript(opts, cb, number, title);
2412
2413
  const script = [header, ...body];
2413
2414
  const finish = () => {
2414
2415
  const costUsd = 0.3 + Math.random();
@@ -2445,13 +2446,13 @@ function fakeSession(opts, cb, fullName) {
2445
2446
  }
2446
2447
  if (kind === "fix") {
2447
2448
  const pr = repos.get(fullName)?.pulls.find((p) => p.number === number);
2448
- if (pr) {
2449
+ if (pr && pushes) {
2449
2450
  pr.headSha = fakeSha();
2450
2451
  pr.mergeState = "CLEAN";
2451
2452
  pr.mergeable = "MERGEABLE";
2452
2453
  runChecks(pr, false);
2453
2454
  }
2454
- cb.log([{ kind: "text", text: `\u25CF Fixed PR #${number} and pushed. Ready for another QA round.` }]);
2455
+ cb.log([{ kind: "text", text: pushes ? `\u25CF Fixed PR #${number} and pushed. Ready for another QA round.` : `\u25CF The toolbar wraps on narrow screens now. Ready for another QA round.` }]);
2455
2456
  cb.finished({ ok: true, text: "", costUsd, turns, errors: [] });
2456
2457
  return;
2457
2458
  }
@@ -3443,6 +3444,26 @@ function specialtySlug(s) {
3443
3444
  }
3444
3445
  var specialtyLabel = (slug) => `swarm:${slug}`;
3445
3446
 
3447
+ // server/fixOutcome.ts
3448
+ var MAX_FIX_FAILURES = 2;
3449
+ function fixOutcome(rec, head, minutes, counts = true) {
3450
+ const pr = rec.prNumber;
3451
+ const before = rec.fixReason === "conflict" ? rec.passedSha ?? rec.testedSha : rec.testedSha;
3452
+ if (rec.fixReason !== "checks" && head && before && head === before) {
3453
+ const sessionFailures = rec.sessionFailures + (counts ? 1 : 0);
3454
+ const needsHuman = sessionFailures >= MAX_FIX_FAILURES;
3455
+ return {
3456
+ set: needsHuman ? { status: "needs-human", sessionFailures, mergeNote: rec.fixReason === "conflict" ? "the conflict was never resolved" : "the fix was never pushed" } : { status: "failed", sessionFailures },
3457
+ log: { kind: "error", text: `\u2717 No new commits were pushed for PR #${pr}` },
3458
+ pushed: false
3459
+ };
3460
+ }
3461
+ if (rec.fixReason === "checks" || rec.fixReason === "conflict") {
3462
+ 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 };
3463
+ }
3464
+ 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 };
3465
+ }
3466
+
3446
3467
  // server/httpError.ts
3447
3468
  var HttpError = class extends Error {
3448
3469
  constructor(status, message) {
@@ -3484,6 +3505,34 @@ function mergeStep(pr, rec, now2, opts) {
3484
3505
  return { do: "merge", set };
3485
3506
  }
3486
3507
 
3508
+ // server/qaOrphans.ts
3509
+ function orphanedQa(qa, agents, busy) {
3510
+ return qa.filter(
3511
+ (q) => q.status === "testing" && !agents.some((a) => a.id === q.qaAgentId && busy.includes(a.status) && a.task === "qa" && a.repoId === q.repoId && a.prNumber === q.prNumber)
3512
+ );
3513
+ }
3514
+
3515
+ // server/restartRecovery.ts
3516
+ function followKeptCli(a, cli) {
3517
+ if (!cli?.busy || a.status === "preparing") return false;
3518
+ return !(a.sessionId && cli.resumeId && cli.resumeId !== a.sessionId);
3519
+ }
3520
+ function resumesAfterRestart(a, preparing, demo) {
3521
+ return !demo && !preparing && a.task !== "qa" && !!a.sessionId && !!a.branch;
3522
+ }
3523
+ function resumeNote(a, fix) {
3524
+ const yours = "Uncommitted changes in your worktree are yours to finish.";
3525
+ if (a.task === "fix" && a.prNumber && a.branch) {
3526
+ const job = fix?.fixReason === "conflict" ? `resolving the merge conflicts on PR #${a.prNumber}` : fix?.fixReason === "checks" ? `fixing the failing checks on PR #${a.prNumber}` : `fixing PR #${a.prNumber}${fix ? ` after QA round ${fix.round}` : ""}`;
3527
+ return `The office server restarted while you were ${job}. Finish the fix, commit, and push with: git push origin HEAD:${a.branch}. ${yours}`;
3528
+ }
3529
+ if (a.task === "issue" && a.issueNumber && a.branch) {
3530
+ const pr = a.prNumber ? `push with: git push origin ${a.branch} to update PR #${a.prNumber}` : `push with: git push -u origin ${a.branch}, and open the pull request with "Closes #${a.issueNumber}" (or push to it if it's already open)`;
3531
+ return `The office server restarted while you were working on issue #${a.issueNumber}${a.issueTitle ? ` (${a.issueTitle})` : ""} on branch ${a.branch}. Finish it, commit, ${pr}. ${yours}`;
3532
+ }
3533
+ return "The office server restarted while you were working. Check the state of your worktree and continue where you left off.";
3534
+ }
3535
+
3487
3536
  // server/previews.ts
3488
3537
  var PREVIEW_BASE_PORT = 6300;
3489
3538
  var LOG_KEEP = 200;
@@ -4220,12 +4269,14 @@ var Swarm = class {
4220
4269
  return [];
4221
4270
  });
4222
4271
  const carryOn = [];
4272
+ const preparing = /* @__PURE__ */ new Set();
4223
4273
  for (const a of this.state.agents) {
4224
4274
  if (!BUSY.includes(a.status)) continue;
4225
- if (back.some((c) => c.agentId === a.id && c.busy)) {
4275
+ if (followKeptCli(a, back.find((c) => c.agentId === a.id))) {
4226
4276
  carryOn.push(a);
4227
4277
  continue;
4228
4278
  }
4279
+ if (a.status === "preparing") preparing.add(a);
4229
4280
  a.status = "stopped";
4230
4281
  a.lastError = "The swarm server restarted while this agent was working.";
4231
4282
  interrupted.push(a);
@@ -4261,7 +4312,8 @@ var Swarm = class {
4261
4312
  this.previews.clearOrphans(this.state.repos)
4262
4313
  ]);
4263
4314
  for (const r of this.state.repos) void this.previews.refreshDefault(r);
4264
- this.recover(interrupted);
4315
+ this.recover(interrupted, preparing);
4316
+ for (const rec of orphanedQa(this.state.qa, this.state.agents, BUSY)) this.setQa(rec, { status: "queued", qaAgentId: null });
4265
4317
  this.officeHead = await this.backend.office.head();
4266
4318
  const updated = await this.backend.office.takeLastUpdate().catch(() => null);
4267
4319
  if (updated) await this.reportUpdate(updated);
@@ -4275,22 +4327,23 @@ var Swarm = class {
4275
4327
  this.save();
4276
4328
  setTimeout(() => this.schedule(), 1e3);
4277
4329
  }
4278
- /** Agents cut off by a server restart pick their Claude Code session back up (QA and demo agents start over). */
4279
- recover(agents) {
4330
+ /**
4331
+ * Agents cut off by a server restart pick their session back up, told what they were doing. QA, demo agents and
4332
+ * tasks still being prepared start over from the queue.
4333
+ */
4334
+ recover(agents, preparing) {
4280
4335
  for (const a of agents) {
4281
- if (a.task === "qa" || this.backend.demo || !a.sessionId || !a.branch) {
4336
+ const fix = a.task === "fix" ? this.state.qa.find((q) => q.devAgentId === a.id && q.status === "fixing") : void 0;
4337
+ if (!resumesAfterRestart(a, preparing.has(a), this.backend.demo)) {
4282
4338
  this.appendLog(a, [{ kind: "system", text: "\u21BA The office server restarted. Starting over from the queue." }]);
4283
4339
  const rec = a.task === "qa" ? this.state.qa.find((q) => q.qaAgentId === a.id && q.status === "testing") : void 0;
4284
4340
  if (rec) this.setQa(rec, { status: "queued" });
4285
- const fix = a.task === "fix" ? this.state.qa.find((q) => q.devAgentId === a.id && q.status === "fixing") : void 0;
4286
4341
  if (fix) this.setQa(fix, { status: "failed" });
4287
4342
  this.clearTask(a);
4288
4343
  continue;
4289
4344
  }
4290
4345
  if (this.slotsFull()) continue;
4291
- void this.message(a.id, "The office server restarted while you were working. Check the state of your worktree and continue where you left off.").catch(
4292
- (err) => console.warn(`could not resume ${a.name}`, err)
4293
- );
4346
+ void this.message(a.id, resumeNote(a, fix)).catch((err) => console.warn(`could not resume ${a.name}`, err));
4294
4347
  }
4295
4348
  }
4296
4349
  // ---------- views ----------
@@ -5256,7 +5309,7 @@ Note from the manager: ${note}` : ""
5256
5309
  void this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0);
5257
5310
  if (result.interrupted) this.interrupted(a);
5258
5311
  if (a.task === "qa") await this.onQaFinished(a, repo, result);
5259
- else if (a.task === "fix") this.onFixFinished(a, repo, result);
5312
+ else if (a.task === "fix") await this.onFixFinished(a, repo, result);
5260
5313
  else await this.onIssueFinished(a, repo, result);
5261
5314
  this.emitAgent(a);
5262
5315
  this.save();
@@ -5366,6 +5419,7 @@ Note from the manager: ${note}` : ""
5366
5419
  if (rec && (rec.status === "needs-human" || rec.status === "passed" || rec.status === "failed")) {
5367
5420
  rec.round += 1;
5368
5421
  rec.sessionFailures = 0;
5422
+ rec.mergeNote = null;
5369
5423
  }
5370
5424
  const dev = this.state.agents.find((a) => a.repoId === repo.id && a.role === "dev" && a.task !== "qa" && (a.prNumber === prNumber || a.branch === pr.headRefName));
5371
5425
  this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
@@ -5557,7 +5611,8 @@ ${issue2.body.trim() || "(no description)"}` : ""
5557
5611
  this.setQa(rec, { status: "fixing", devAgentId: dev.id });
5558
5612
  this.beginTask(
5559
5613
  dev,
5560
- { task: "fix", issueNumber: rec.issueNumber, issueTitle: pull?.title ?? `PR #${rec.prNumber}`, branch: headRef, prNumber: rec.prNumber, prUrl: pull?.url ?? null },
5614
+ // The session they held was another task's (QA, say); the author's own is resumed below, from rec.devSessionId.
5615
+ { task: "fix", issueNumber: rec.issueNumber, issueTitle: pull?.title ?? `PR #${rec.prNumber}`, branch: headRef, prNumber: rec.prNumber, prUrl: pull?.url ?? null, sessionId: null },
5561
5616
  rec.fixReason === "conflict" ? `Resolving conflicts on PR #${rec.prNumber}` : rec.fixReason === "checks" ? `Fixing checks on PR #${rec.prNumber}` : `Fixing PR #${rec.prNumber} after QA round ${rec.round}`,
5562
5617
  `Checking out PR #${rec.prNumber}\u2026`
5563
5618
  );
@@ -5602,8 +5657,9 @@ Full report with screenshots: ${rec.commentUrl}` : "",
5602
5657
  const resume = original && rec.devSessionId ? rec.devSessionId : void 0;
5603
5658
  this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }), resume);
5604
5659
  }
5605
- onFixFinished(a, repo, result) {
5660
+ async onFixFinished(a, repo, result) {
5606
5661
  const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
5662
+ const head = rec && result.ok && a.status !== "stopped" && rec.fixReason !== "checks" ? await this.prHead(a, repo, rec.prNumber) : null;
5607
5663
  if (a.status === "stopped") {
5608
5664
  if (rec?.status === "fixing") this.setQa(rec, { status: "failed" });
5609
5665
  return;
@@ -5614,17 +5670,40 @@ Full report with screenshots: ${rec.commentUrl}` : "",
5614
5670
  if (rec) this.setQa(rec, { status: failures >= 2 ? "needs-human" : "failed", sessionFailures: failures });
5615
5671
  return;
5616
5672
  }
5617
- a.status = "done";
5618
- if (rec && (rec.fixReason === "checks" || rec.fixReason === "conflict")) {
5619
- this.appendLog(a, [{ kind: "done", text: `\u2714 PR #${a.prNumber} fixed in ${this.minutes(a)}m. Back in line to merge.` }]);
5620
- this.setQa(rec, { status: "passed", devSessionId: a.sessionId ?? rec.devSessionId, mergeNote: "waiting for fresh checks" });
5621
- this.toast("info", `${a.name} fixed PR #${rec.prNumber}; it merges once it passes again`);
5673
+ if (!rec || !this.state.qa.includes(rec)) {
5674
+ a.status = "done";
5675
+ this.appendLog(a, [{ kind: "done", text: `\u2714 Fix pushed for PR #${a.prNumber} in ${this.minutes(a)}m. Back to QA.` }]);
5622
5676
  return;
5623
5677
  }
5624
- this.appendLog(a, [{ kind: "done", text: `\u2714 Fix pushed for PR #${a.prNumber} in ${this.minutes(a)}m. Back to QA.` }]);
5625
- if (rec) {
5626
- this.setQa(rec, { status: "queued", round: rec.round + 1, devSessionId: a.sessionId ?? rec.devSessionId });
5627
- this.toast("info", `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`);
5678
+ const step = fixOutcome(rec, head, this.minutes(a), !this.limited());
5679
+ if (!step.pushed) {
5680
+ const needsHuman = step.set.status === "needs-human";
5681
+ a.status = "error";
5682
+ a.lastError = `No new commits were pushed for PR #${rec.prNumber}`;
5683
+ this.appendLog(a, [step.log]);
5684
+ this.setQa(rec, step.set);
5685
+ this.toast("error", `${a.name} ended the fix for PR #${rec.prNumber} without pushing a commit${needsHuman ? "; it needs you" : "; it goes back for another try"}`);
5686
+ if (needsHuman) {
5687
+ this.postMessage("office", `\u26A0\uFE0F PR #${rec.prNumber} on ${repo.fullName} went back for fixes, but ${step.set.sessionFailures} fix sessions in a row ended without pushing a commit, so it needs you.`);
5688
+ }
5689
+ return;
5690
+ }
5691
+ a.status = "done";
5692
+ this.appendLog(a, [step.log]);
5693
+ this.setQa(rec, { ...step.set, devSessionId: a.sessionId ?? rec.devSessionId });
5694
+ this.toast(
5695
+ "info",
5696
+ step.set.status === "passed" ? `${a.name} fixed PR #${rec.prNumber}; it merges once it passes again` : `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`
5697
+ );
5698
+ }
5699
+ /** The PR's head commit now; null (with a warning in the agent's log) when GitHub can't be asked. */
5700
+ async prHead(a, repo, prNumber) {
5701
+ try {
5702
+ return (await this.backend.prDetails(repo.fullName, prNumber)).headSha;
5703
+ } catch (err) {
5704
+ console.warn(`could not read the head of ${repo.fullName}#${prNumber}`, err);
5705
+ this.appendLog(a, [{ kind: "system", text: `\u26A0 Could not check PR #${prNumber} for new commits (${oneLine(err)}); taking the fix at its word.` }]);
5706
+ return null;
5628
5707
  }
5629
5708
  }
5630
5709
  /** A message for an agent: sent into their running session, or a follow-up that resumes it. typed: the manager typed it at their CLI's prompt, where it's already running. */
@@ -5945,13 +6024,14 @@ Full report with screenshots: ${rec.commentUrl}` : "",
5945
6024
  const u = this.officeUpdate;
5946
6025
  Object.assign(u, { sent: false, handedOver: false, requested: false, drainingSince: null });
5947
6026
  const interrupted = this.state.agents.filter((a) => BUSY.includes(a.status));
6027
+ const preparing = new Set(interrupted.filter((a) => a.status === "preparing"));
5948
6028
  for (const a of interrupted) {
5949
6029
  Object.assign(a, { status: "stopped", lastError: "The office updated itself while this agent was working." });
5950
6030
  const rt = this.agentRt.get(a.id);
5951
6031
  if (rt) Object.assign(rt, { session: null, currentTool: null });
5952
6032
  }
5953
6033
  this.ensureCeo(interrupted);
5954
- this.recover(interrupted);
6034
+ this.recover(interrupted, preparing);
5955
6035
  interrupted.forEach((a) => this.emitAgent(a));
5956
6036
  await this.reportUpdate(result);
5957
6037
  if (!result.ok) u.failedBehind = u.behind;