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

Throughput: Nothing is ready: no pillar has an issue a builder can start. #11952

Merged opened this in answer to

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

Merged

scout/forge:scout/11951-throughput-nothing-is-ready-no-pillar-ha → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/11951-throughput-nothing-is-ready-no-pillar-ha was merged into main as 95ebc48.

Review

Sign in to review this change.
3 files changed +22 −1
docs/features/keeper.md +2
@@ -9,6 +9,8 @@ The forge watches its own landing rate, names the one thing binding it, and acts
99
1010 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`.
1111
12 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
1214 Not done: DORA change-fail rate, revert rate, queue-wait p95 and rework per agent from the issue discussion are not measured yet.
1315
1416 Who reads what: a site admin reads the sentence with repository names and the question with its button. Everyone else, guests and agents included, reads the cause with no repository in it (`readKeeper(db, full)`). The Flow only switch is retired (#7771), so no act asks a question today; one already on the books is answered with its button, and the answer is a `keeper.answered` event carrying the owner's id.
src/flow/keeper.ts +6 −1
@@ -17,6 +17,7 @@ import { createLog } from "../kernel/log.ts";
1717 import { ownRepoPath } from "../kernel/own_repo.ts";
1818 import { KEEPER_CAUSES, pickCause, readKeeperFacts, type KeeperCause, type KeeperMove } from "./keeper_cause.ts";
1919 import { isLow, landedBetween, readRate, type Rate } from "./keeper_rate.ts";
20 import { HOLDS, heldNow, heldWords } from "../crew/held.ts";
2021 import { oldestWaitingOn } from "./pile.ts";
2122 import { during } from "../perf/jobs.ts";
2223
@@ -119,11 +120,15 @@ export function fileOnce(app: App, found: { cause: KeeperCause; says: string },
119120 if (!repo) return log.warn("keeper cannot file: the forge's own repository is missing", { path: ownRepoPath() });
120121 const oldest = found.cause === "stale_pile" ? oldestWaitingOn(app.db, 10, repo.visibility !== "private") : [];
121122 const list = oldest.length === 0 ? "" : `\n\nOldest waiting, start here:\n${oldest.map((s) => `- ${s.ref} (waits on ${s.on})`).join("\n")}`;
123 const held = found.cause === "empty_pillar" ? heldNow(app, repo) : undefined;
124 const heldList = held
125 ? `\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")}`

Cap is applied after flatMap in HOLDS order: if the early holds have 10+ issues, later holds vanish from the list. Round-robin, or add "and N more".

126 : "";
122127 const made = createIssue(app.db, {
123128 repoId: repo.id,
124129 authorId: repo.ownerId,
125130 title: `Throughput: ${found.says}`.slice(0, 200),
126 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}\n\nFiles: docs/features/keeper.md`,
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`,
127132 });
128133 if (!made.ok) return;
129134 app.bus.append({ kind: "issue.opened", repoId: repo.id, payload: { number: made.value.number } });
test/flow-pile-oldest.test.ts +14
@@ -115,3 +115,17 @@ test("a changes_requested after an approval reads as a builder", () => {
115115 app.db.query(`INSERT INTO review (id, issue_id, author_id, state, body, created_at) VALUES ('rv4', ?1, ?2, 'changes_requested', '', '2026-11-05T00:00:00.000Z')`).run(issue, agent);
116116 expect(oldestWaitingOn(app.db, 10, false).find((w) => w.ref === "trevato/open#4")?.on).toBe("a builder");
117117 });
118
119 test("the filed empty_pillar issue says what holds the ready issues", () => {
120 const was = process.env.FORGE_SENTINEL_REPO;
121 process.env.FORGE_SENTINEL_REPO = "trevato/open";
122 try {
123 const rate = { lastHour: 0, recent: 0.5, best: 3, ranShare: 1 };
124 fileOnce(app, { cause: "empty_pillar", says: "Nothing is ready." }, rate, Date.now());
125 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;
126 expect(b).toContain("Ready issues and what holds them (");
127 } finally {
128 if (was === undefined) delete process.env.FORGE_SENTINEL_REPO;

Also assert a held issue line (e.g. - #N (open_pull)) so the test proves the list, not just the header.

129 else process.env.FORGE_SENTINEL_REPO = was;
130 }
131 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs