cubefarm 0.3.10 → 0.3.11

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-DxPWlxj7.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-DZng_ER1.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-qBB5_EtY.css">
13
13
  </head>
14
14
  <body>
@@ -164,8 +164,8 @@ async function issueState(fullName, number) {
164
164
  return null;
165
165
  }
166
166
  }
167
- async function closeIssue(fullName, number, comment) {
168
- await gh(["issue", "close", String(number), "-R", fullName, "--reason", "completed", "--comment", comment]);
167
+ async function closeIssue(fullName, number, comment, reason2 = "completed") {
168
+ await gh(["issue", "close", String(number), "-R", fullName, "--reason", reason2, "--comment", comment]);
169
169
  }
170
170
  async function editIssue(fullName, number, edit) {
171
171
  const args = ["issue", "edit", String(number), "-R", fullName];
@@ -188,6 +188,9 @@ async function mergePull(fullName, number, method, headSha) {
188
188
  async function updateBranch(fullName, number) {
189
189
  await gh(["pr", "update-branch", String(number), "-R", fullName], { timeoutMs: 6e4 });
190
190
  }
191
+ async function rerunFailedJobs(fullName, runIds) {
192
+ for (const id of runIds) await gh(["run", "rerun", String(id), "--failed", "-R", fullName], { timeoutMs: 6e4 });
193
+ }
191
194
  async function closePull(fullName, number) {
192
195
  await gh(["pr", "close", String(number), "-R", fullName]);
193
196
  }
@@ -893,6 +896,8 @@ function describeOfficeTool(action, input) {
893
896
  return `\u{1F44B} propose_let_go ${String(input.agent_id ?? "")}`;
894
897
  case "file_issue":
895
898
  return `\u{1F4DD} file_issue "${clip(String(input.title ?? ""), 70)}"${floor}${input.specialty ? ` \xB7 ${input.specialty}` : ""}`;
899
+ case "close_issue":
900
+ return `\u{1F5C2}\uFE0F close_issue #${String(input.number ?? "?")}${floor}`;
896
901
  case "route_issue":
897
902
  return `\u{1F500} route_issue #${String(input.number ?? "?")}${floor}${input.specialty !== void 0 ? ` \xB7 ${input.specialty || "no specialty"}` : ""}${Array.isArray(input.depends_on) ? ` \xB7 depends on ${input.depends_on.map((n) => `#${n}`).join(", ") || "nothing"}` : ""}`;
898
903
  }
@@ -2899,6 +2904,7 @@ var realBackend = {
2899
2904
  closeIssue,
2900
2905
  mergePull,
2901
2906
  updateBranch,
2907
+ rerunFailedJobs,
2902
2908
  closePull,
2903
2909
  prForBranch,
2904
2910
  prDetails,
@@ -2943,13 +2949,14 @@ var bareRepos = /* @__PURE__ */ new Set();
2943
2949
  var mergedSinceSync = /* @__PURE__ */ new Map();
2944
2950
  var closedIssues = /* @__PURE__ */ new Set();
2945
2951
  var fakeSha = () => crypto6.randomBytes(20).toString("hex");
2952
+ var runSeq = 1e3;
2946
2953
  function runChecks(pr, fail = Math.random() < 0.2) {
2947
2954
  Object.assign(pr, { checks: "pending", pendingChecks: ["CI / build", "Vercel"], failedChecks: [] });
2948
2955
  setTimeout(() => {
2949
2956
  Object.assign(pr, {
2950
2957
  checks: fail ? "failing" : "passing",
2951
2958
  pendingChecks: [],
2952
- failedChecks: fail ? [{ name: "CI / build", url: `${pr.url}/checks` }] : []
2959
+ failedChecks: fail ? [{ name: "CI / build", url: `${pr.url.replace(/\/pull\/\d+$/, "")}/actions/runs/${++runSeq}/job/1` }] : []
2953
2960
  });
2954
2961
  }, 12e3 + Math.random() * 1e4);
2955
2962
  }
@@ -3346,6 +3353,11 @@ function createDemoBackend() {
3346
3353
  Object.assign(pr, { headSha: fakeSha(), mergeState: "CLEAN" });
3347
3354
  runChecks(pr, false);
3348
3355
  },
3356
+ rerunFailedJobs: async (fullName, runIds) => {
3357
+ for (const pr of repos.get(fullName)?.pulls ?? []) {
3358
+ if (pr.failedChecks.some((c) => runIds.some((id) => c.url?.includes(`/actions/runs/${id}/`)))) runChecks(pr, false);
3359
+ }
3360
+ },
3349
3361
  closePull: async (fullName, number) => {
3350
3362
  const pr = repos.get(fullName)?.pulls.find((p) => p.number === number);
3351
3363
  if (pr) pr.state = "CLOSED";
@@ -3728,6 +3740,13 @@ Acceptance criteria:
3728
3740
  async chat(text) {
3729
3741
  const s2 = await status();
3730
3742
  await think("Reading your message.");
3743
+ const close = text.match(/\bclose #(\d+)[\s,:;.-]*(.*)/i);
3744
+ const closeFloor = close && (s2.floors.find((f) => f.backlog.some((i) => i.number === Number(close[1]))) ?? s2.floors[0]);
3745
+ if (close && closeFloor) {
3746
+ const reason2 = close[2].trim() || "No longer wanted.";
3747
+ const out = await use("close_issue", { floor: closeFloor.floor, number: Number(close[1]), reason: reason2 });
3748
+ return out.startsWith("Refused") ? `I couldn't close #${close[1]}: ${out.replace(/^Refused: /, "")}` : `Done: ${out} I left "${short(reason2, 80)}" on it as a comment.`;
3749
+ }
3731
3750
  const people = s2.floors.reduce((n, f) => n + f.team.length, 0);
3732
3751
  const issues = s2.floors.reduce((n, f) => n + f.backlog.length, 0);
3733
3752
  const pending2 = s2.pendingProposals.length;
@@ -3851,6 +3870,7 @@ import { z } from "zod";
3851
3870
  var MAX_MERGE_FIXES = 3;
3852
3871
  var CHECKS_ALERT_MS = 30 * 6e4;
3853
3872
  var MERGE_RETRY_MS = 10 * 6e4;
3873
+ var RERUN_GRACE_MS = 2 * 6e4;
3854
3874
  function mergeStep(pr, rec, now2, opts) {
3855
3875
  if (pr.isDraft) return { do: "wait", note: "a draft: waiting", set: {} };
3856
3876
  if (!opts.detailed && (pr.mergeable === "UNKNOWN" || pr.mergeState === "UNKNOWN")) return { do: "details", set: {} };
@@ -3861,8 +3881,12 @@ function mergeStep(pr, rec, now2, opts) {
3861
3881
  if (pr.mergeable === "CONFLICTING" || pr.mergeState === "DIRTY") {
3862
3882
  return { do: "send-back", reason: "conflict", instructions: `It conflicts with ${opts.base}.`, needsHuman, set };
3863
3883
  }
3884
+ const rerun = rec.rerunSha === pr.headSha;
3864
3885
  if (pr.checks === "failing") {
3865
3886
  const instructions = pr.failedChecks.map((c) => `- ${c.name}${c.url ? `: ${c.url}` : ""}`).join("\n");
3887
+ const runIds = failedRunIds(pr.failedChecks);
3888
+ if (!rerun && runIds.length) return { do: "rerun", runIds, reason: "checks", instructions, needsHuman, set };
3889
+ if (rerun && rec.rerunAt != null && now2 - rec.rerunAt < RERUN_GRACE_MS) return { do: "wait", note: "re-running a failed check", set };
3866
3890
  return { do: "send-back", reason: "checks", instructions, needsHuman, set };
3867
3891
  }
3868
3892
  if (pr.checks === "pending") {
@@ -3870,7 +3894,8 @@ function mergeStep(pr, rec, now2, opts) {
3870
3894
  if (rec.pendingSince == null) set.pendingSince = since;
3871
3895
  const alert = now2 - since > CHECKS_ALERT_MS && !rec.alerted;
3872
3896
  if (alert) set.alerted = true;
3873
- return { do: "wait", note: `waiting for checks: ${pr.pendingChecks.join(", ") || "running"}`, alert, set };
3897
+ const running = pr.pendingChecks.join(", ") || "running";
3898
+ return { do: "wait", note: rerun ? `re-running a failed check: ${running}` : `waiting for checks: ${running}`, alert, set };
3874
3899
  }
3875
3900
  set.pendingSince = null;
3876
3901
  if (pr.mergeable === "UNKNOWN") return { do: "wait", note: "GitHub is checking it can merge", set };
@@ -3878,6 +3903,10 @@ function mergeStep(pr, rec, now2, opts) {
3878
3903
  if (rec.mergeRetryAt && now2 < rec.mergeRetryAt) return { do: "wait", set };
3879
3904
  return { do: "merge", set };
3880
3905
  }
3906
+ function failedRunIds(failed2) {
3907
+ const ids = failed2.map((c) => c.url?.match(/\/actions\/runs\/(\d+)/)?.[1]).filter((id) => !!id);
3908
+ return [...new Set(ids)].map(Number);
3909
+ }
3881
3910
 
3882
3911
  // server/qaOutcome.ts
3883
3912
  var MAX_QA_ROUNDS = 3;
@@ -4083,6 +4112,16 @@ function createOfficeTools(h) {
4083
4112
  depends_on: z2.array(z2.number().int().positive()).max(10).optional().describe("Issues it waits for; [] for none. Not for an issue in progress.")
4084
4113
  },
4085
4114
  (a) => run2(() => h.routeIssue(a))
4115
+ ),
4116
+ tool(
4117
+ "close_issue",
4118
+ 'Close an open issue that is superseded or no longer wanted, as "not planned", with your reason as a comment. Not while an open pull request closes it. Nothing is deleted.',
4119
+ {
4120
+ floor: z2.number().int(),
4121
+ number: z2.number().int().positive().describe("The issue number"),
4122
+ reason: z2.string().min(1).max(1e3).describe('Posted as a comment, e.g. "Superseded by #152."')
4123
+ },
4124
+ (a) => run2(() => h.closeIssue(a))
4086
4125
  )
4087
4126
  ];
4088
4127
  const server2 = createSdkMcpServer({ name: "office", version: "1.0.0", tools: defs });
@@ -4112,7 +4151,9 @@ function ceoSystemPrompt(o) {
4112
4151
  "Your job is to run the company, not to write code:",
4113
4152
  "- Understand each project: what it is, its stack, how far along it is, and what kind of people it needs. Projects differ a lot. A static marketing site, a 3D browser game and a REST API need different specialists and different QA.",
4114
4153
  `- Shape each floor's team. Propose specialists with a specific title and a job description written for this project. Keep teams lean: agents on the same coding agent share one subscription's usage limits${o.sessionLimit ? ` and at most ${o.sessionLimit} sessions run at once` : ""}, so a floor rarely needs more than ${o.teamCap} people. When the manager asks for a bigger team, follow that, up to the floor's free seats (seats in company_status). Propose letting people go when a floor is clearly overstaffed or a specialty is no longer needed.`,
4115
- "- Plan the work: turn a floor's brief into small, well-specified GitHub issues, one agent-session each, with acceptance criteria. Route each to a specialty. The office hands issues out itself: a free specialist gets first pick of their specialty, and otherwise any free developer takes the next issue that can start, so a specialty is a preference, not a lock.",
4154
+ "- Plan the work: turn a floor's brief into well-specified GitHub issues with acceptance criteria. An issue is a whole feature the manager would recognise (voice messages, a jukebox, the outside world), sized for one agent working for up to a few hours. Agents have large context windows and handle long jobs. Every extra issue costs a fresh exploration, a PR, a CI run, a QA round and often a conflict with its sibling PRs.",
4155
+ "- Split a feature only when its parts are truly independent AND touch different files, or when one risky foundation part should land and be tested first. Never split a feature just to give idle developers something to do: parallel work comes from different features side by side. Unrelated small fixes are still their own issues.",
4156
+ "- Route each issue to a specialty. The office hands issues out itself: a free specialist gets first pick of their specialty, and otherwise any free developer takes the next issue that can start, so a specialty is a preference, not a lock.",
4116
4157
  "- Write each floor's QA brief: what QA testers must check for this kind of project (for a 3D game: the canvas renders, controls respond, frame rate is smooth; for a website: links, phone layout, accessibility; for an API: status codes, validation, error cases).",
4117
4158
  "",
4118
4159
  "How you work:",
@@ -4127,7 +4168,8 @@ function ceoSystemPrompt(o) {
4127
4168
  '- Titles are specific ("Three.js graphics engineer", not "Developer"). A specialty is a short lowercase slug ("graphics", "gameplay", "frontend", "backend", "content", "a11y", "devops"). Only route an issue to a specialty that someone on the floor has, or that you are proposing to hire.',
4128
4169
  "- Before proposing a hire, check the floor and the pending proposals for someone who already covers it. If the manager declined a similar proposal (recentDecisions), do not propose it again unless something has changed, and say what.",
4129
4170
  `- ${o.hiring === "auto" ? "Hiring is on auto: proposals within the team cap are approved immediately, so be deliberate." : "The manager approves every hire, so explain each reason in a sentence or two they can decide on."}`,
4130
- `- Issues: plan for parallel work. What keeps a floor busy is the number of issues that can start right now (capacity.issuesReadyToStart in company_status); aim for at least one per developer. Write "Depends on #N" only when an issue truly cannot start until #N's code is merged, because it waits until #N is closed. Keep dependency chains to two steps at most, keep foundation issues small, and split big pieces into parts that can be built side by side. The office starts the issues that hold up others first. Do not duplicate open issues: fix an existing issue's specialty or dependencies with route_issue. File at most 12 issues per job, or per message from the manager.`,
4171
+ `- Issues: QA is usually the scarcer resource. When PRs queue for QA (capacity.prsAwaitingQa in company_status), file fewer, bigger issues, not more. Most briefs need 1 to 4 issues. Write "Depends on #N" only when an issue truly cannot start until #N's code is merged, because it waits until #N is closed. Keep dependency chains to two steps at most. The office starts the issues that hold up others first. Do not duplicate open issues: fix an existing issue's specialty or dependencies with route_issue. File at most 12 issues per job, or per message from the manager.`,
4172
+ "- Close an issue that is superseded or no longer wanted with close_issue, not by making it wait for another issue.",
4131
4173
  "- When company.usage in company_status says pacing or paused, Claude's usage is running low and the office is finishing open work first: file only what is needed next, not a whole milestone.",
4132
4174
  "- Your final message goes straight to the manager's phone. Keep it short and plain: what you found, what you proposed, what you filed, and any question you need answered. No headings, no tables."
4133
4175
  ].join("\n");
@@ -4153,7 +4195,7 @@ ${floor.backlog === 0 ? "The backlog is empty: plan the first milestone as issue
4153
4195
  "",
4154
4196
  "Plan the next milestone toward it:",
4155
4197
  `- Read the current code and the ${floor.backlog} open issues first, so you build on what exists and do not duplicate anything.`,
4156
- "- If the repository is empty or nearly empty, the first issue sets up a small project skeleton, and the others depend on it. Everything after that should be able to run side by side.",
4198
+ "- One issue per whole feature. Split a feature only when its parts are independent and touch different files, or a risky foundation part should land first. Only for an empty or nearly empty repository does a skeleton issue come first, with the others depending on it.",
4157
4199
  "- File the issues, each routed to a specialty.",
4158
4200
  "- Make sure the floor has the specialists those issues need; propose hires if not.",
4159
4201
  "- Update the floor profile and QA brief if the brief changes what the project is."
@@ -4162,7 +4204,7 @@ ${floor.backlog === 0 ? "The backlog is empty: plan the first milestone as issue
4162
4204
  return [
4163
4205
  "Periodic review of the company. For every floor, look at:",
4164
4206
  "- floors without a profile or QA brief: study them and write one",
4165
- "- backlog against the team (capacity): fewer issues ready to start than free developers, long dependency chains, a specialty with a long queue (fix those with route_issue)",
4207
+ "- backlog against the team (capacity): long dependency chains or a specialty with a long queue (fix those with route_issue), or PRs piling up in QA (then plan fewer, bigger issues). Idle developers are not a reason to slice features: they cover QA.",
4166
4208
  "- pull requests stuck in QA or marked as needing a human",
4167
4209
  "- floors with a brief and an empty backlog: plan the next milestone",
4168
4210
  "Propose hires or let-gos only when clearly justified. If nothing needs doing, reply with one short sentence saying so."
@@ -4247,13 +4289,28 @@ function planRoute(r) {
4247
4289
  const loop = deps.find((d) => reaches(d, r.number, waits));
4248
4290
  if (loop !== void 0) throw new Error(`#${loop} already waits for #${r.number}, directly or through other issues, so that would be a cycle.`);
4249
4291
  const depth = waitsDepth(r.number, waits) + (holdUps(after).get(r.number)?.chain ?? 0);
4250
- if (depth > 2) throw new Error(`That makes a dependency chain ${depth} steps deep through #${r.number}. Keep chains to 2 steps at most: split the work so more of it can start side by side.`);
4292
+ if (depth > 2) throw new Error(`That makes a dependency chain ${depth} steps deep through #${r.number}. Keep chains to 2 steps at most: fold the dependent pieces into one issue instead of splitting further.`);
4251
4293
  if (body2 !== issue2.body) plan.body = body2;
4252
4294
  done.push(deps.length ? `depends on ${deps.map((d) => `#${d}`).join(", ")}` : "no dependencies");
4253
4295
  }
4254
4296
  plan.summary = `#${r.number} on floor ${r.floor}: ${done.join(", ")}.`;
4255
4297
  return plan;
4256
4298
  }
4299
+ function floorCapacity(f) {
4300
+ const open = new Set(f.issues.map((i) => i.number));
4301
+ const prs = new Set(f.openPrs);
4302
+ return {
4303
+ issuesWaitingOnOthers: f.issues.filter((i) => !f.inProgress(i.number) && blockers(i.body, open).length > 0).length,
4304
+ longestDependencyChain: Math.max(0, ...[...holdUps(f.issues).values()].map((w) => w.chain)),
4305
+ prsAwaitingQa: f.qa.filter((q) => prs.has(q.prNumber) && (q.status === "queued" || q.status === "testing")).length
4306
+ };
4307
+ }
4308
+ function checkCloseIssue(r) {
4309
+ if (r.state === "CLOSED") throw new HttpError(404, `#${r.number} on floor ${r.floor} is already closed.`);
4310
+ if (r.state !== "OPEN") throw new HttpError(404, `There is no open issue #${r.number} on floor ${r.floor}.`);
4311
+ const pr = r.pulls.find((p) => p.state === "OPEN" && p.closesIssues.includes(r.number));
4312
+ if (pr) throw new HttpError(409, `PR #${pr.number} closes #${r.number}. Close or finish that pull request first.`);
4313
+ }
4257
4314
  function reaches(from, to, waits, seen = /* @__PURE__ */ new Set()) {
4258
4315
  if (from === to) return true;
4259
4316
  if (seen.has(from)) return false;
@@ -4275,10 +4332,18 @@ async function setUpDesk(term, steps) {
4275
4332
 
4276
4333
  // server/fixOutcome.ts
4277
4334
  var MAX_FIX_FAILURES = 2;
4278
- function fixOutcome(rec, head, minutes, counts = true) {
4335
+ function fixOutcome(rec, head, minutes, counts = true, checks = null) {
4279
4336
  const pr = rec.prNumber;
4280
4337
  const before = rec.fixReason === "conflict" ? rec.passedSha ?? rec.testedSha : rec.testedSha;
4281
- if (rec.fixReason !== "checks" && head && before && head === before) {
4338
+ const unpushed = rec.fixReason !== "checks" && head && before && head === before;
4339
+ if (unpushed && rec.fixReason !== "conflict" && rec.qaChecks === "failing" && (checks === "passing" || checks === "pending")) {
4340
+ return {
4341
+ set: { status: "queued", round: rec.round + 1, retests: rec.retests + 1, sessionFailures: 0 },
4342
+ log: { kind: "done", text: `\u2714 PR #${pr}'s failed checks were re-run in ${minutes}m. Back to QA.` },
4343
+ pushed: true
4344
+ };
4345
+ }
4346
+ if (unpushed) {
4282
4347
  const sessionFailures = rec.sessionFailures + (counts ? 1 : 0);
4283
4348
  const needsHuman = sessionFailures >= MAX_FIX_FAILURES;
4284
4349
  return {
@@ -4638,7 +4703,7 @@ ${repo.qaBrief}`] : [],
4638
4703
  testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
4639
4704
  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).",
4640
4705
  "5. You may write throwaway scripts to probe behaviour, but do not commit them.",
4641
- "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.",
4706
+ "A merge conflict with the default branch is not a fail, nor is a red check unrelated to this change (a flake or an outage: say why): 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, and re-runs a failed check before anyone fixes it.",
4642
4707
  "",
4643
4708
  "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."
4644
4709
  ].join("\n");
@@ -5104,7 +5169,8 @@ var Swarm = class {
5104
5169
  proposeHire: (a) => this.proposeHire(a),
5105
5170
  proposeLetGo: (a) => this.proposeLetGo(a),
5106
5171
  fileIssue: (a) => this.fileIssue(a),
5107
- routeIssue: (a) => this.routeIssue(a)
5172
+ routeIssue: (a) => this.routeIssue(a),
5173
+ closeIssue: (a) => this.closeIssue(a)
5108
5174
  });
5109
5175
  }
5110
5176
  ceoIssues = new IssueCap(MAX_ISSUES_PER_JOB);
@@ -5180,6 +5246,9 @@ var Swarm = class {
5180
5246
  pendingSince: q.pendingSince ?? null,
5181
5247
  mergeRetryAt: q.mergeRetryAt ?? null,
5182
5248
  alerted: q.alerted ?? false,
5249
+ rerunSha: q.rerunSha ?? null,
5250
+ rerunAt: q.rerunAt ?? null,
5251
+ qaChecks: q.qaChecks ?? null,
5183
5252
  mergeNote: null
5184
5253
  })),
5185
5254
  requests: loaded.requests ?? [],
@@ -5680,6 +5749,16 @@ var Swarm = class {
5680
5749
  setTimeout(() => this.schedule(), 200);
5681
5750
  return false;
5682
5751
  }
5752
+ if (step.do === "rerun") {
5753
+ try {
5754
+ await this.backend.rerunFailedJobs(repo.fullName, step.runIds);
5755
+ } catch (err) {
5756
+ console.warn(`could not re-run the failed checks of ${repo.fullName}#${pr.number}`, err);
5757
+ return this.sendBack(repo, rec, step.reason, step.instructions, step.needsHuman);
5758
+ }
5759
+ this.setQa(rec, { rerunSha: pr.headSha, rerunAt: Date.now(), pendingSince: null, alerted: false, mergeNote: "re-running a failed check" });
5760
+ return false;
5761
+ }
5683
5762
  if (step.do === "send-back") return this.sendBack(repo, rec, step.reason, step.instructions, step.needsHuman);
5684
5763
  if (step.do === "wait") {
5685
5764
  if (step.alert) {
@@ -6425,7 +6504,10 @@ Note from the manager: ${note}` : ""
6425
6504
  retests: 0,
6426
6505
  pendingSince: null,
6427
6506
  mergeRetryAt: null,
6428
- alerted: false
6507
+ alerted: false,
6508
+ rerunSha: null,
6509
+ rerunAt: null,
6510
+ qaChecks: null
6429
6511
  };
6430
6512
  this.state.qa.push(rec);
6431
6513
  this.setQa(rec, {});
@@ -6554,7 +6636,7 @@ ${issue2.body.trim() || "(no description)"}` : ""
6554
6636
  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` }]);
6555
6637
  let commentUrl = null;
6556
6638
  if (rec) {
6557
- const pull = !pass && lastQaRound(rec) ? await this.backend.prDetails(repo.fullName, rec.prNumber).catch(() => null) : null;
6639
+ const pull = !pass ? await this.backend.prDetails(repo.fullName, rec.prNumber).catch(() => null) : null;
6558
6640
  const next = qaOutcome(pass, rec, pull);
6559
6641
  try {
6560
6642
  this.appendLog(a, [{ kind: "system", text: "\u{1F4CE} Uploading evidence and posting the QA report on the PR\u2026" }]);
@@ -6579,7 +6661,8 @@ ${issue2.body.trim() || "(no description)"}` : ""
6579
6661
  mergeNote: null,
6580
6662
  pendingSince: null,
6581
6663
  mergeRetryAt: null,
6582
- alerted: false
6664
+ alerted: false,
6665
+ qaChecks: pull?.checks ?? null
6583
6666
  });
6584
6667
  if (next === "conflict") this.sendBack(repo, rec, "conflict", conflictFixInstructions(fixInstructions, repo.defaultBranch), false);
6585
6668
  this.toast(
@@ -6690,7 +6773,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
6690
6773
  }
6691
6774
  async onFixFinished(a, repo, result) {
6692
6775
  const rec = this.state.qa.find((q) => q.repoId === repo.id && q.prNumber === a.prNumber);
6693
- const head = rec && result.ok && a.status !== "stopped" && rec.fixReason !== "checks" ? await this.prHead(a, repo, rec.prNumber) : null;
6776
+ const current = rec && result.ok && a.status !== "stopped" && rec.fixReason !== "checks" ? await this.prHead(a, repo, rec.prNumber) : null;
6694
6777
  if (a.status === "stopped") {
6695
6778
  if (rec?.status === "fixing") this.setQa(rec, { status: "failed" });
6696
6779
  return;
@@ -6706,7 +6789,7 @@ Full report with screenshots: ${rec.commentUrl}` : "",
6706
6789
  this.appendLog(a, [{ kind: "done", text: `\u2714 Fix pushed for PR #${a.prNumber} in ${this.minutes(a)}m. Back to QA.` }]);
6707
6790
  return;
6708
6791
  }
6709
- const step = fixOutcome(rec, head, this.minutes(a), !this.limited());
6792
+ const step = fixOutcome(rec, current?.headSha ?? null, this.minutes(a), !this.limited(), current?.checks ?? null);
6710
6793
  if (!step.pushed) {
6711
6794
  const needsHuman = step.set.status === "needs-human";
6712
6795
  a.status = "error";
@@ -6724,13 +6807,13 @@ Full report with screenshots: ${rec.commentUrl}` : "",
6724
6807
  this.setQa(rec, { ...step.set, devSessionId: a.sessionId ?? rec.devSessionId });
6725
6808
  this.toast(
6726
6809
  "info",
6727
- 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`
6810
+ step.set.status === "passed" ? `${a.name} fixed PR #${rec.prNumber}; it merges once it passes again` : step.set.retests ? `${a.name} re-ran the failed checks on PR #${rec.prNumber}; QA re-tests it` : `${a.name} pushed fixes for PR #${rec.prNumber}; QA round ${rec.round} is queued`
6728
6811
  );
6729
6812
  }
6730
- /** The PR's head commit now; null (with a warning in the agent's log) when GitHub can't be asked. */
6813
+ /** The PR's head commit and checks now; null (with a warning in the agent's log) when GitHub can't be asked. */
6731
6814
  async prHead(a, repo, prNumber) {
6732
6815
  try {
6733
- return (await this.backend.prDetails(repo.fullName, prNumber)).headSha;
6816
+ return await this.backend.prDetails(repo.fullName, prNumber);
6734
6817
  } catch (err) {
6735
6818
  console.warn(`could not read the head of ${repo.fullName}#${prNumber}`, err);
6736
6819
  this.appendLog(a, [{ kind: "system", text: `\u26A0 Could not check PR #${prNumber} for new commits (${oneLine(err)}); taking the fix at its word.` }]);
@@ -7488,8 +7571,12 @@ ${t}`);
7488
7571
  developers: this.state.agents.filter((a) => a.repoId === r.id && a.role === "dev").length,
7489
7572
  developersFree: this.available(r, "dev").length,
7490
7573
  issuesReadyToStart: this.readyIssues(r).length,
7491
- issuesWaitingOnOthers: rt.issues.filter((i) => blockers(i.body, open).length > 0).length,
7492
- longestDependencyChain: Math.max(0, ...[...holdUps(rt.issues).values()].map((w) => w.chain))
7574
+ ...floorCapacity({
7575
+ issues: rt.issues,
7576
+ inProgress: (n) => this.issueTaken(r, n),
7577
+ openPrs: rt.pulls.filter((p) => p.state === "OPEN").map((p) => p.number),
7578
+ qa: this.state.qa.filter((q) => q.repoId === r.id)
7579
+ })
7493
7580
  },
7494
7581
  team: this.state.agents.filter((a) => a.repoId === r.id).map((a) => ({
7495
7582
  id: a.id,
@@ -7720,6 +7807,20 @@ _Filed by ${this.ceo().name}, the cubefarm CEO._`;
7720
7807
  }
7721
7808
  return plan.summary;
7722
7809
  }
7810
+ /** Close an open issue as not planned, with the CEO's reason as its comment (see checkCloseIssue for what is refused). */
7811
+ async closeIssue(x) {
7812
+ const repo = this.floorRepo(x.floor);
7813
+ const n = Number(x.number);
7814
+ const rt = this.repoRt.get(repo.id);
7815
+ const reason2 = String(x.reason ?? "").trim().slice(0, 1e3);
7816
+ if (!reason2) throw new HttpError(400, "Say why the issue is closing: the reason is posted on it as a comment.");
7817
+ const state = rt?.issues.some((i) => i.number === n) ? "OPEN" : await this.backend.issueState(repo.fullName, n).catch(() => null);
7818
+ checkCloseIssue({ floor: repo.floor, number: n, state, pulls: rt?.pulls ?? [] });
7819
+ await this.backend.closeIssue(repo.fullName, n, reason2, "not planned");
7820
+ await this.syncRepo(repo.id);
7821
+ this.postMessage("office", `\u{1F5C2}\uFE0F ${this.ceo().name} closed #${n} on floor ${repo.floor}: ${reason2.split("\n")[0].slice(0, 200)}`);
7822
+ return `Closed #${n} on floor ${repo.floor} as not planned.`;
7823
+ }
7723
7824
  };
7724
7825
 
7725
7826
  // server/index.ts