Public: anyone can read this; the crew can change it.

Why a run waits for a builder is said from one module, read by finishing and requested #5703

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/5652-why-a-run-waits-for-a-builder-is-said-fr → main 1 commit fork

  • Merged. mason/5652-why-a-run-waits-for-a-builder-is-said-fr was merged into main as 5f1d8c0.

Review

Sign in to review this change.
5 files changed +58 −6
src/crew/finishing.ts +3 −2
@@ -44,6 +44,7 @@ import { readConflictRoom } from "./conflict_room.ts";
4444 import { CHECK_FAILED_LIMIT, lastHumanWord, queueAgentRun } from "./dispatch.ts";
4545 import { HANDOVER_NOTE } from "./guard.ts";
4646 import { pickBuilder } from "./route.ts";
47 import { NO_BUILDER_ON_CREW, WAITS_FOR_BUILDER_SLOT } from "./words.ts";
4748
4849 const log = createLog("crew");
4950
@@ -145,9 +146,9 @@ function finishOne(
145146 builders.find((b) => b.user.id === issue.assigneeId) ??
146147 builders.find((b) => b.user.id === issue.authorId) ??
147148 pickBuilder(issue, builders, new Map());
148 if (builder === undefined) return not("no builder on the crew");
149 if (builder === undefined) return not(NO_BUILDER_ON_CREW);
149150 if (failedUpdates(app, issue, builder) >= CHECK_FAILED_LIMIT) return handOver(app, repo, pull, issue, files);
150 if (slots <= 0) return not("waits for a builder slot");
151 if (slots <= 0) return not(WAITS_FOR_BUILDER_SLOT);
151152 if (room <= 0) return not("waits for room in the check lane");
152153 const run = queueAgentRun(app, repo, issue, builder, "conflict", undefined, {
153154 notBefore: new Date().toISOString(),
src/crew/requested.ts +4 −3
@@ -36,6 +36,7 @@ import { queueAgentRun } from "./dispatch.ts";
3636 import { HANDOVER_NOTE, handoverOf } from "./guard.ts";
3737 import { greenHead, sendToReview } from "./review.ts";
3838 import { pickBuilder } from "./route.ts";
39 import { NO_BUILDER_ON_CREW, WAITS_FOR_BUILDER_SLOT } from "./words.ts";
3940
4041 const log = createLog("crew");
4142
@@ -60,7 +61,7 @@ function reasonOf(why: string): string {
6061 if (why === "a run is in flight" || why.startsWith("a review of the new head")) return "a run is in flight";
6162 if (why === "a person holds it" || why.startsWith("held,")) return "held";
6263 if (why.includes("a person")) return "handed to a person";
63 if (why === "waits for a builder slot") return "waits for a slot";
64 if (why === WAITS_FOR_BUILDER_SLOT) return "waits for a slot";
6465 if (why === "the new head is not green yet") return "waits for a check";
6566 if (why.startsWith("past the review rounds") || why.startsWith("a verdict is already") || why.startsWith("a run since"))
6667 return "settled";
@@ -114,7 +115,7 @@ export async function moveRequested(
114115 const move = verdictIsStale(requested)
115116 ? await askAgain(app, repo, requested, now)
116117 : slots <= 0
117 ? notMoved(requested, "waits for a builder slot")
118 ? notMoved(requested, WAITS_FOR_BUILDER_SLOT)
118119 : followUp(app, repo, requested, builders);
119120 if (move.did === "queued feedback") slots--;
120121 moves.push(move);
@@ -180,7 +181,7 @@ function followUp(
180181 // run counted here ended without one: one is asked again, two go to a person.
181182 const tried = startedSince(app, pull.id, requested.at);
182183 if (tried >= FOLLOW_UPS) return handOverPushless(app, repo, requested, issue);
183 if (builders.length === 0) return notMoved(requested, "no builder on the crew");
184 if (builders.length === 0) return notMoved(requested, NO_BUILDER_ON_CREW);
184185 const assignee = issue.assigneeId;
185186 let builder = builders.find((b) => b.user.id === assignee);
186187 let assigned = false;
src/crew/words.ts added +12
@@ -0,0 +1,12 @@
1 /**
2 * Why a run waits for a builder, said once (#5652). The finishing pass
3 * (`finishing.ts`) and the changes-requested pass (`requested.ts`) both
4 * report these as a pull's `why`, and every door — page, API, MCP,
5 * terminal — reads them from there, so they read the same everywhere.
6 */
7
8 /** Every builder slot this tick is spent: the pull moves on a later one. */
9 export const WAITS_FOR_BUILDER_SLOT = "waits for a builder slot";
10
11 /** The repository's crew has no agent in the builder role to run it. */
12 export const NO_BUILDER_ON_CREW = "no builder on the crew";
src/web/studio/shelf.ts +1 −1
@@ -168,7 +168,7 @@ export async function shelfRows(
168168 db,
169169 repos.map((r) => r.id),
170170 );
171 // A tell nobody can pick up (no builder on the crew) is stuck, not quiet —
171 // A tell nobody can pick up (the crew has no builder) is stuck, not quiet —
172172 // both reads batched across the whole shelf, never once per row.
173173 const repoIds = repos.map((r) => r.id);
174174 const withBuilder = reposWithBuilder(db, repoIds);
test/crew-wait-words.test.ts added +38
@@ -0,0 +1,38 @@
1 /**
2 * Why a run waits for a builder is said from one module (#5652):
3 * `src/crew/words.ts` holds each sentence, and the passes that report it
4 * import it rather than typing it again. Read as text, so a copy typed
5 * back into either file fails here.
6 */
7
8 import { describe, expect, test } from "bun:test";
9 import { readFileSync } from "node:fs";
10 import { join } from "node:path";
11 import { NO_BUILDER_ON_CREW, WAITS_FOR_BUILDER_SLOT } from "../src/crew/words.ts";
12
13 const ROOT = join(import.meta.dir, "..");
14 const read = (path: string): string => readFileSync(join(ROOT, path), "utf8");
15
16 const SENTENCES = [
17 ["WAITS_FOR_BUILDER_SLOT", WAITS_FOR_BUILDER_SLOT, "waits for a builder slot"],
18 ["NO_BUILDER_ON_CREW", NO_BUILDER_ON_CREW, "no builder on the crew"],
19 ] as const;
20
21 const READERS = ["src/crew/finishing.ts", "src/crew/requested.ts"] as const;
22
23 describe("why a run waits for a builder, said once", () => {
24 test("the module says each sentence, word for word", () => {
25 for (const [, said, words] of SENTENCES) expect(said).toBe(words);
26 });
27
28 for (const file of READERS) {
29 test(`${file} imports the sentences and types none of them`, () => {
30 const text = read(file);
31 expect(text).toContain('from "./words.ts"');
32 for (const [name, said] of SENTENCES) {
33 expect(text).toContain(name);
34 expect(text).not.toContain(said);
35 }
36 });
37 }
38 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs