Public: anyone can read this; the crew can change it.
Throughput: 5 open crew pulls have a red check. #12258
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/12120-throughput-5-open-crew-pulls-have-a-red → main 1 commit fork 1 thread · 1 unresolved
4 files
changed +46
−2
docs/features/keeper.md +2
| @@ -9,6 +9,8 @@ The forge watches its own landing rate, names the one thing binding it, and acts | ||
| 9 | 9 | |
| 10 | 10 | Where it shows: one line on home and on the boards panel, `keeper` in the MCP `flow` report, the terminal's Flow dialog (the line, the question, and `l` for its one button after a confirm) and its crew scorecard ("Forge per day" rows), and "Forge per day" (hours running, landed) on the crew scorecard and in `scorecard` (`keeperDays`). Every attempt is a row in `keeper_attempt`. |
| 11 | 11 | |
| 12 | A `red_checks` issue lists the red pulls, oldest first, so whoever picks it up knows which to rework. | |
| 13 | ||
| 12 | 14 | An `empty_pillar` issue lists the ready issues and the hold on each (`landed`, `open_pull`, `stalled`...), so a person sees why nothing can start. |
| 13 | 15 | |
| 14 | 16 | Not done: DORA change-fail rate, revert rate, queue-wait p95 and rework per agent from the issue discussion are not measured yet. |
src/flow/keeper.ts +4 −2
| @@ -18,7 +18,7 @@ import { ownRepoPath } from "../kernel/own_repo.ts"; | ||
| 18 | 18 | import { KEEPER_CAUSES, pickCause, readKeeperFacts, type KeeperCause, type KeeperMove } from "./keeper_cause.ts"; |
| 19 | 19 | import { isLow, landedBetween, readRate, type Rate } from "./keeper_rate.ts"; |
| 20 | 20 | import { HOLDS, heldNow, heldWords } from "../crew/held.ts"; |
| 21 | import { oldestWaitingOn } from "./pile.ts"; | |
| 21 | import { oldestWaitingOn, redPulls } from "./pile.ts"; | |
| 22 | 22 | import { during } from "../perf/jobs.ts"; |
| 23 | 23 | |
| 24 | 24 | const log = createLog("keeper"); |
| @@ -120,6 +120,8 @@ export function fileOnce(app: App, found: { cause: KeeperCause; says: string }, | ||
| 120 | 120 | if (!repo) return log.warn("keeper cannot file: the forge's own repository is missing", { path: ownRepoPath() }); |
| 121 | 121 | const oldest = found.cause === "stale_pile" ? oldestWaitingOn(app.db, 10, repo.visibility !== "private") : []; |
| 122 | 122 | const list = oldest.length === 0 ? "" : `\n\nOldest waiting, start here:\n${oldest.map((s) => `- ${s.ref} (waits on ${s.on})`).join("\n")}`; |
| 123 | const reds = found.cause === "red_checks" ? redPulls(app.db, 10, repo.visibility !== "private") : []; | |
| 124 | const redList = reds.length === 0 ? "" : `\n\nRed, oldest first, start here:\n${reds.map((r) => `- ${r}`).join("\n")}`; | |
| 123 | 125 | const held = found.cause === "empty_pillar" ? heldNow(app, repo) : undefined; |
| 124 | 126 | const heldList = held |
| 125 | 127 | ? `\n\nReady issues and what holds them (${heldWords(held)}):\n${HOLDS.flatMap((h) => held.by[h].slice(0, 5).map((i) => `- #${i.number} (${h})`)).slice(0, 10).join("\n")}` |
| @@ -128,7 +130,7 @@ export function fileOnce(app: App, found: { cause: KeeperCause; says: string }, | ||
| 128 | 130 | repoId: repo.id, |
| 129 | 131 | authorId: repo.ownerId, |
| 130 | 132 | title: `Throughput: ${found.says}`.slice(0, 200), |
| 131 | body: `The throughput keeper found this binding landings (${rate.recent.toFixed(1)} an hour against a best of ${rate.best.toFixed(1)}) and cannot act on it itself.\n\n${found.says}${list}${heldList}\n\nFiles: docs/features/keeper.md`, | |
| 133 | body: `The throughput keeper found this binding landings (${rate.recent.toFixed(1)} an hour against a best of ${rate.best.toFixed(1)}) and cannot act on it itself.\n\n${found.says}${list}${redList}${heldList}\n\nFiles: docs/features/keeper.md`, | |
| 132 | 134 | }); |
| 133 | 135 | if (!made.ok) return; |
| 134 | 136 | app.bus.append({ kind: "issue.opened", repoId: repo.id, payload: { number: made.value.number } }); |
src/flow/pile.ts +25
| @@ -107,3 +107,28 @@ export function oldestWaitingOn(db: Database, limit: number, publicOnly = true): | ||
| 107 | 107 | } |
| 108 | 108 | return out; |
| 109 | 109 | } |
| 110 | ||
| 111 | /** | |
| 112 | * The open crew pulls whose latest check on the current head failed, oldest | |
| 113 | * first, each as `owner/repo#n`. A filed "red check" issue names them so the | |
| 114 | * one who picks it up knows which pulls to rework. Private repositories are | |
| 115 | * left out unless `publicOnly` is false. | |
| 116 | */ | |
| 117 | export function redPulls(db: Database, limit: number, publicOnly = true): string[] { | |
| 118 | const cap = Math.max(0, Math.floor(limit)); | |
| 119 | if (cap === 0) return []; | |
| 120 | return db | |
| 121 | .query<{ ref: string }, [number, number]>( | |
| 122 | `SELECT o.username || '/' || r.name || '#' || i.number AS ref | |
| 123 | FROM issue i JOIN pull p ON p.issue_id = i.id JOIN repo r ON r.id = i.repo_id | |
| 124 | JOIN user o ON o.id = r.owner_id JOIN user a ON a.id = i.author_id | |
| 125 | WHERE i.state = 'open' AND p.merged_at IS NULL AND (?1 = 0 OR r.visibility = 'public') | |
| 126 | AND a.is_agent = 1 AND COALESCE(i.origin_author, '') = '' | |
| 127 | AND EXISTS (SELECT 1 FROM repo_crew c WHERE c.repo_id = r.id AND c.user_id = a.id) | |
| 128 | AND COALESCE((SELECT c.state FROM run c WHERE c.kind = 'check' AND c.issue_id = i.id | |
| 129 | AND c.commit_oid = p.head_oid ORDER BY c.id DESC LIMIT 1) = 'failed', 0) | |
| 130 | ORDER BY i.created_at ASC, i.id ASC LIMIT ?2`, | |
| 131 | ) | |
| 132 | .all(publicOnly ? 1 : 0, cap) | |
| 133 | .map((r) => r.ref); | |
| 134 | } |
test/flow-pile-oldest.test.ts +15
| @@ -129,3 +129,18 @@ test("the filed empty_pillar issue says what holds the ready issues", () => { | ||
| 129 | 129 | else process.env.FORGE_SENTINEL_REPO = was; |
| 130 | 130 | } |
| 131 | 131 | }); |
| 132 | ||
| 133 | test("the filed red_checks issue names the red pulls, oldest first", () => { | |
| 134 | const was = process.env.FORGE_SENTINEL_REPO; | |
| 135 | process.env.FORGE_SENTINEL_REPO = "trevato/open"; | |
| 136 | try { | |
| 137 | const rate = { lastHour: 0, recent: 0.5, best: 3, ranShare: 1 }; | |
| 138 | fileOnce(app, { cause: "red_checks", says: "2 open crew pulls have a red check." }, rate, Date.now()); | |
| 139 | const b = app.db.query<{ body: string }, []>("SELECT body FROM issue WHERE title LIKE 'Throughput:%' ORDER BY created_at DESC, rowid DESC LIMIT 1").get()!.body; | |
| 140 | expect(b).toContain("Red, oldest first, start here:\n- trevato/open#2"); | |
| 141 | expect(b).not.toContain("Oldest waiting"); | |
| 142 | } finally { | |
| 143 | if (was === undefined) delete process.env.FORGE_SENTINEL_REPO; | |
| 144 | else process.env.FORGE_SENTINEL_REPO = was; | |
| 145 | } | |
| 146 | }); |
new above
offline
This asserts only one entry, so the ordering promised by the test name is not proven. Seed a second red pull with a later created_at and assert the order.