cubefarm 0.3.5 → 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-CeoPkKws.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) {
@@ -3491,6 +3512,27 @@ function orphanedQa(qa, agents, busy) {
3491
3512
  );
3492
3513
  }
3493
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
+
3494
3536
  // server/previews.ts
3495
3537
  var PREVIEW_BASE_PORT = 6300;
3496
3538
  var LOG_KEEP = 200;
@@ -4227,12 +4269,14 @@ var Swarm = class {
4227
4269
  return [];
4228
4270
  });
4229
4271
  const carryOn = [];
4272
+ const preparing = /* @__PURE__ */ new Set();
4230
4273
  for (const a of this.state.agents) {
4231
4274
  if (!BUSY.includes(a.status)) continue;
4232
- if (back.some((c) => c.agentId === a.id && c.busy)) {
4275
+ if (followKeptCli(a, back.find((c) => c.agentId === a.id))) {
4233
4276
  carryOn.push(a);
4234
4277
  continue;
4235
4278
  }
4279
+ if (a.status === "preparing") preparing.add(a);
4236
4280
  a.status = "stopped";
4237
4281
  a.lastError = "The swarm server restarted while this agent was working.";
4238
4282
  interrupted.push(a);
@@ -4268,7 +4312,7 @@ var Swarm = class {
4268
4312
  this.previews.clearOrphans(this.state.repos)
4269
4313
  ]);
4270
4314
  for (const r of this.state.repos) void this.previews.refreshDefault(r);
4271
- this.recover(interrupted);
4315
+ this.recover(interrupted, preparing);
4272
4316
  for (const rec of orphanedQa(this.state.qa, this.state.agents, BUSY)) this.setQa(rec, { status: "queued", qaAgentId: null });
4273
4317
  this.officeHead = await this.backend.office.head();
4274
4318
  const updated = await this.backend.office.takeLastUpdate().catch(() => null);
@@ -4283,22 +4327,23 @@ var Swarm = class {
4283
4327
  this.save();
4284
4328
  setTimeout(() => this.schedule(), 1e3);
4285
4329
  }
4286
- /** Agents cut off by a server restart pick their Claude Code session back up (QA and demo agents start over). */
4287
- 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) {
4288
4335
  for (const a of agents) {
4289
- 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)) {
4290
4338
  this.appendLog(a, [{ kind: "system", text: "\u21BA The office server restarted. Starting over from the queue." }]);
4291
4339
  const rec = a.task === "qa" ? this.state.qa.find((q) => q.qaAgentId === a.id && q.status === "testing") : void 0;
4292
4340
  if (rec) this.setQa(rec, { status: "queued" });
4293
- const fix = a.task === "fix" ? this.state.qa.find((q) => q.devAgentId === a.id && q.status === "fixing") : void 0;
4294
4341
  if (fix) this.setQa(fix, { status: "failed" });
4295
4342
  this.clearTask(a);
4296
4343
  continue;
4297
4344
  }
4298
4345
  if (this.slotsFull()) continue;
4299
- 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(
4300
- (err) => console.warn(`could not resume ${a.name}`, err)
4301
- );
4346
+ void this.message(a.id, resumeNote(a, fix)).catch((err) => console.warn(`could not resume ${a.name}`, err));
4302
4347
  }
4303
4348
  }
4304
4349
  // ---------- views ----------
@@ -5264,7 +5309,7 @@ Note from the manager: ${note}` : ""
5264
5309
  void this.backend.releaseDesk(repo.fullName, this.agentSlug(a), this.port(a)).catch(() => void 0);
5265
5310
  if (result.interrupted) this.interrupted(a);
5266
5311
  if (a.task === "qa") await this.onQaFinished(a, repo, result);
5267
- else if (a.task === "fix") this.onFixFinished(a, repo, result);
5312
+ else if (a.task === "fix") await this.onFixFinished(a, repo, result);
5268
5313
  else await this.onIssueFinished(a, repo, result);
5269
5314
  this.emitAgent(a);
5270
5315
  this.save();
@@ -5374,6 +5419,7 @@ Note from the manager: ${note}` : ""
5374
5419
  if (rec && (rec.status === "needs-human" || rec.status === "passed" || rec.status === "failed")) {
5375
5420
  rec.round += 1;
5376
5421
  rec.sessionFailures = 0;
5422
+ rec.mergeNote = null;
5377
5423
  }
5378
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));
5379
5425
  this.queueQa(repo, prNumber, dev ?? null, pr.closesIssues[0] ?? null);
@@ -5565,7 +5611,8 @@ ${issue2.body.trim() || "(no description)"}` : ""
5565
5611
  this.setQa(rec, { status: "fixing", devAgentId: dev.id });
5566
5612
  this.beginTask(
5567
5613
  dev,
5568
- { 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 },
5569
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}`,
5570
5617
  `Checking out PR #${rec.prNumber}\u2026`
5571
5618
  );
@@ -5610,8 +5657,9 @@ Full report with screenshots: ${rec.commentUrl}` : "",
5610
5657
  const resume = original && rec.devSessionId ? rec.devSessionId : void 0;
5611
5658
  this.startAgentSession(dev, repo, cwd, prompt, this.buildSystemAppend(dev, repo, cwd, headRef, { pr: rec.prNumber, headRef }), resume);
5612
5659
  }
5613
- onFixFinished(a, repo, result) {
5660
+ async onFixFinished(a, repo, result) {
5614
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;
5615
5663
  if (a.status === "stopped") {
5616
5664
  if (rec?.status === "fixing") this.setQa(rec, { status: "failed" });
5617
5665
  return;
@@ -5622,17 +5670,40 @@ Full report with screenshots: ${rec.commentUrl}` : "",
5622
5670
  if (rec) this.setQa(rec, { status: failures >= 2 ? "needs-human" : "failed", sessionFailures: failures });
5623
5671
  return;
5624
5672
  }
5625
- a.status = "done";
5626
- if (rec && (rec.fixReason === "checks" || rec.fixReason === "conflict")) {
5627
- this.appendLog(a, [{ kind: "done", text: `\u2714 PR #${a.prNumber} fixed in ${this.minutes(a)}m. Back in line to merge.` }]);
5628
- this.setQa(rec, { status: "passed", devSessionId: a.sessionId ?? rec.devSessionId, mergeNote: "waiting for fresh checks" });
5629
- 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.` }]);
5630
5676
  return;
5631
5677
  }
5632
- this.appendLog(a, [{ kind: "done", text: `\u2714 Fix pushed for PR #${a.prNumber} in ${this.minutes(a)}m. Back to QA.` }]);
5633
- if (rec) {
5634
- this.setQa(rec, { status: "queued", round: rec.round + 1, devSessionId: a.sessionId ?? rec.devSessionId });
5635
- 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;
5636
5707
  }
5637
5708
  }
5638
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. */
@@ -5953,13 +6024,14 @@ Full report with screenshots: ${rec.commentUrl}` : "",
5953
6024
  const u = this.officeUpdate;
5954
6025
  Object.assign(u, { sent: false, handedOver: false, requested: false, drainingSince: null });
5955
6026
  const interrupted = this.state.agents.filter((a) => BUSY.includes(a.status));
6027
+ const preparing = new Set(interrupted.filter((a) => a.status === "preparing"));
5956
6028
  for (const a of interrupted) {
5957
6029
  Object.assign(a, { status: "stopped", lastError: "The office updated itself while this agent was working." });
5958
6030
  const rt = this.agentRt.get(a.id);
5959
6031
  if (rt) Object.assign(rt, { session: null, currentTool: null });
5960
6032
  }
5961
6033
  this.ensureCeo(interrupted);
5962
- this.recover(interrupted);
6034
+ this.recover(interrupted, preparing);
5963
6035
  interrupted.forEach((a) => this.emitAgent(a));
5964
6036
  await this.reportUpdate(result);
5965
6037
  if (!result.ok) u.failedBehind = u.behind;