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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5652-why-a-run-waits-for-a-builder-is-said-fr → main 1 commit fork
5 files
changed +58
−6
src/crew/finishing.ts +3 −2
| @@ -44,6 +44,7 @@ import { readConflictRoom } from "./conflict_room.ts"; | ||
| 44 | 44 | import { CHECK_FAILED_LIMIT, lastHumanWord, queueAgentRun } from "./dispatch.ts"; |
| 45 | 45 | import { HANDOVER_NOTE } from "./guard.ts"; |
| 46 | 46 | import { pickBuilder } from "./route.ts"; |
| 47 | import { NO_BUILDER_ON_CREW, WAITS_FOR_BUILDER_SLOT } from "./words.ts"; | |
| 47 | 48 | |
| 48 | 49 | const log = createLog("crew"); |
| 49 | 50 | |
| @@ -145,9 +146,9 @@ function finishOne( | ||
| 145 | 146 | builders.find((b) => b.user.id === issue.assigneeId) ?? |
| 146 | 147 | builders.find((b) => b.user.id === issue.authorId) ?? |
| 147 | 148 | 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); | |
| 149 | 150 | 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); | |
| 151 | 152 | if (room <= 0) return not("waits for room in the check lane"); |
| 152 | 153 | const run = queueAgentRun(app, repo, issue, builder, "conflict", undefined, { |
| 153 | 154 | notBefore: new Date().toISOString(), |
src/crew/requested.ts +4 −3
| @@ -36,6 +36,7 @@ import { queueAgentRun } from "./dispatch.ts"; | ||
| 36 | 36 | import { HANDOVER_NOTE, handoverOf } from "./guard.ts"; |
| 37 | 37 | import { greenHead, sendToReview } from "./review.ts"; |
| 38 | 38 | import { pickBuilder } from "./route.ts"; |
| 39 | import { NO_BUILDER_ON_CREW, WAITS_FOR_BUILDER_SLOT } from "./words.ts"; | |
| 39 | 40 | |
| 40 | 41 | const log = createLog("crew"); |
| 41 | 42 | |
| @@ -60,7 +61,7 @@ function reasonOf(why: string): string { | ||
| 60 | 61 | if (why === "a run is in flight" || why.startsWith("a review of the new head")) return "a run is in flight"; |
| 61 | 62 | if (why === "a person holds it" || why.startsWith("held,")) return "held"; |
| 62 | 63 | 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"; | |
| 64 | 65 | if (why === "the new head is not green yet") return "waits for a check"; |
| 65 | 66 | if (why.startsWith("past the review rounds") || why.startsWith("a verdict is already") || why.startsWith("a run since")) |
| 66 | 67 | return "settled"; |
| @@ -114,7 +115,7 @@ export async function moveRequested( | ||
| 114 | 115 | const move = verdictIsStale(requested) |
| 115 | 116 | ? await askAgain(app, repo, requested, now) |
| 116 | 117 | : slots <= 0 |
| 117 | ? notMoved(requested, "waits for a builder slot") | |
| 118 | ? notMoved(requested, WAITS_FOR_BUILDER_SLOT) | |
| 118 | 119 | : followUp(app, repo, requested, builders); |
| 119 | 120 | if (move.did === "queued feedback") slots--; |
| 120 | 121 | moves.push(move); |
| @@ -180,7 +181,7 @@ function followUp( | ||
| 180 | 181 | // run counted here ended without one: one is asked again, two go to a person. |
| 181 | 182 | const tried = startedSince(app, pull.id, requested.at); |
| 182 | 183 | 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); | |
| 184 | 185 | const assignee = issue.assigneeId; |
| 185 | 186 | let builder = builders.find((b) => b.user.id === assignee); |
| 186 | 187 | 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( | ||
| 168 | 168 | db, |
| 169 | 169 | repos.map((r) => r.id), |
| 170 | 170 | ); |
| 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 — | |
| 172 | 172 | // both reads batched across the whole shelf, never once per row. |
| 173 | 173 | const repoIds = repos.map((r) => r.id); |
| 174 | 174 | 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 | }); |
new above
offline