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

Throughput: 54 open crew pulls are waiting on a builder or a reviewer. #9511

Merged opened this in answer to

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

Merged

scout/forge:scout/9417-throughput-54-open-crew-pulls-are-waitin → main 3 commits fork 4 threads · 4 unresolved

  • Merged. scout/9417-throughput-54-open-crew-pulls-are-waitin was merged into main as d5cf759.

Review

Sign in to review this change.
4 files changed +124 −4
docs/features/keeper.md +1 −1
@@ -4,7 +4,7 @@ The forge watches its own landing rate, names the one thing binding it, and acts
44
55 1. **Measure** (`keeper_rate.ts`): pulls landed per hour over the last 3 h against the best 3 h rate of the week before, and the share of the last 24 hours that ran (a landing or a builder run started). Low is under half the best and under one an hour.
66 2. **Find** (`keeper_cause.ts`): the first of `ceiling`, `usage` ("resets at HH:MM UTC"), `unassigned`, `empty_pillar`, `stale_reds`, `red_checks`, `stale_pile`, `capacity`. It asks the dispatcher's own questions (`heldNow`, `atCeiling`, `pace`).
7 3. **Act**: `unassigned` sweeps assigned issues with no run. `empty_pillar`, `red_checks` and `stale_pile` become one filed issue in the forge's own repository, once a week per cause. `ceiling`, `usage`, `capacity` and `stale_reds` (the crew's red-pull sweep is already at it) are only said.
7 3. **Act**: `unassigned` sweeps assigned issues with no run. `empty_pillar`, `red_checks` and `stale_pile` become one filed issue in the forge's own repository, once a week per cause (`stale_pile`'s issue lists the ten oldest waiting pulls, red and conflicted ones left out, so a builder knows where to start). `ceiling`, `usage`, `capacity` and `stale_reds` (the crew's red-pull sweep is already at it) are only said.
88 4. **Check** after 2 h: landings rose and the act stands, or it is undone, the cause is left alone for 6 h and the owner is asked one question with one button (`POST /flow/keeper/answer`).
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`.
src/flow/keeper.ts +6 −3
@@ -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 { oldestWaiting } from "./pile.ts";
2021
2122 const log = createLog("keeper");
2223 const HOUR_MS = 3_600_000;
@@ -110,16 +111,18 @@ function settle(app: App, now: number): void {
110111 }
111112 }
112113
113 /** Files one issue for a cause the keeper cannot act on, in the forge's own repository. */
114 function fileOnce(app: App, found: { cause: KeeperCause; says: string }, rate: Rate, now: number): void {
114 /** Files one issue for a cause the keeper cannot act on, in the forge's own repository; private repositories are named in it only when that repository is private too. */
115 export function fileOnce(app: App, found: { cause: KeeperCause; says: string }, rate: Rate, now: number): void {
115116 const [owner, name] = ownRepoPath().split("/");
116117 const repo = owner && name ? getRepo(app.db, owner, name) : undefined;
117118 if (!repo) return log.warn("keeper cannot file: the forge's own repository is missing", { path: ownRepoPath() });
119 const oldest = found.cause === "stale_pile" ? oldestWaiting(app.db, 10, repo.visibility !== "private") : [];
outdated

No publicOnly passed, and no test covers this filing path. Pass it, and test fileOnce for stale_pile.

120 const list = oldest.length === 0 ? "" : `\n\nOldest waiting, start here:\n${oldest.map((s) => `- ${s}`).join("\n")}`;
118121 const made = createIssue(app.db, {
119122 repoId: repo.id,
120123 authorId: repo.ownerId,
121124 title: `Throughput: ${found.says}`.slice(0, 200),
122 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}\n\nFiles: docs/features/keeper.md`,
125 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`,
123126 });
124127 if (!made.ok) return;
125128 app.bus.append({ kind: "issue.opened", repoId: repo.id, payload: { number: made.value.number } });
src/flow/pile.ts +24
@@ -63,3 +63,27 @@ export function pileSays(pile: Pile): string {
6363 if (pile.total === 0) return "No open crew pulls.";
6464 return `${pile.total} open crew ${pile.total === 1 ? "pull" : "pulls"}, each counted once in the first pile it fits: ${pile.red} red, ${pile.conflicted} conflicted, ${pile.changesAsked} changes asked, ${pile.approved} approved and waiting, ${pile.awaitingReview} awaiting review.`;
6565 }
66
67 /**
68 * The oldest open crew pulls that wait on a person or a reviewer (not red,
69 * not conflicted), as `owner/repo#n`: what a filed "waiting" issue names so a
70 * builder can start at the first one. Private repositories are left out
71 * unless the caller says `publicOnly` is false: the safe value is the default.
72 */
73 export function oldestWaiting(db: Database, limit: number, publicOnly = true): string[] {
outdated

Default publicOnly=false leaks private repo names into a filed issue. Make the safe value the default and have keeper.ts decide explicitly.

74 const rows = db
75 .query<{ repo: string; number: number }, [number, number]>(
76 `SELECT o.username || '/' || r.name AS repo, i.number AS number
77 FROM issue i JOIN pull p ON p.issue_id = i.id JOIN repo r ON r.id = i.repo_id
78 JOIN user o ON o.id = r.owner_id JOIN user a ON a.id = i.author_id
79 WHERE i.state = 'open' AND p.merged_at IS NULL AND (?1 = 0 OR r.visibility = 'public')
80 AND a.is_agent = 1 AND COALESCE(i.origin_author, '') = ''
81 AND EXISTS (SELECT 1 FROM repo_crew c WHERE c.repo_id = r.id AND c.user_id = a.id)
82 AND NOT COALESCE((SELECT c.state FROM run c WHERE c.kind = 'check' AND c.issue_id = i.id
83 AND c.commit_oid = p.head_oid ORDER BY c.id DESC LIMIT 1) = 'failed', 0)
84 ORDER BY i.created_at ASC, i.id ASC LIMIT ?2`,
85 )
86 .all(publicOnly ? 1 : 0, Math.max(0, Math.floor(limit)));
87 const conflicted = new Set(conflictedPulls(db, publicOnly).map((p) => `${p.repo}#${p.number}`));
88 return rows.map((r) => `${r.repo}#${r.number}`).filter((s) => !conflicted.has(s));
89 }
test/flow-pile-oldest.test.ts added +93
@@ -0,0 +1,93 @@
1 /** The filed "waiting" issue names the oldest waiting pulls, not the red ones. */

Missing cases: a conflicted pull is left out, a private repo is left out with publicOnly, and the keeper issue body contains the list.

2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtempSync, rmSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { addCrew, createAgentRow } from "../src/core/agents.ts";
8 import { createRepo } from "../src/core/repos.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { id } from "../src/kernel/id.ts";
11 import { fileOnce } from "../src/flow/keeper.ts";
12 import { oldestWaiting } from "../src/flow/pile.ts";
13
14 let root: string;
15 let app: App;
16 let repoId: string;
17 let agent: string;
18
19 beforeAll(async () => {
20 root = mkdtempSync(join(tmpdir(), "forge-oldest-"));
21 app = createApp({ dataDir: root, port: 0 });
22 const o = await createUser(app.db, { username: "trevato", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true, adminIfFirst: true });
23 if (!o.ok) throw new Error(o.error);
24 const a = await createUser(app.db, { origin: "seed", username: "atlas", email: "[email protected]", agent: true });
25 if (!a.ok) throw new Error("agent");
26 agent = a.value.id;
27 createAgentRow(app.db, { userId: agent, brief: "plan", createdBy: o.value.id });
28 const r = await createRepo(app.db, app.reposRoot, { ownerId: o.value.id, name: "open", visibility: "public", description: "r" });
29 if (!r.ok) throw new Error(r.error);
30 repoId = r.value.id;
31 addCrew(app.db, repoId, agent, o.value.id);
32 });
33 afterAll(() => {
34 app.close();
35 rmSync(root, { recursive: true, force: true });
36 });
37
38 function pull(number: number, createdAt: string): string {
39 const issue = id();
40 app.db
41 .query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 't', '', ?4, 'open', ?5)`)
42 .run(issue, repoId, number, agent, createdAt);
43 app.db.query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id) VALUES (?1, 'main', 'b', '', 'h', ?2)`).run(issue, repoId);
44 return issue;
45 }
46
47 test("oldest first, red pulls left out, capped at the limit", () => {
48 pull(1, "2026-11-01T00:00:00.000Z");
49 const red = pull(2, "2026-11-01T01:00:00.000Z");
50 pull(3, "2026-11-01T02:00:00.000Z");
51 pull(4, "2026-11-01T03:00:00.000Z");
52 app.db.query(`INSERT INTO run (id, repo_id, kind, issue_id, commit_oid, state, reason) VALUES ('0001x', ?1, 'check', ?2, 'h', 'failed', '')`).run(repoId, red);
53 expect(oldestWaiting(app.db, 2)).toEqual(["trevato/open#1", "trevato/open#3"]);
54 expect(oldestWaiting(app.db, 10)).toEqual(["trevato/open#1", "trevato/open#3", "trevato/open#4"]);
55 expect(oldestWaiting(app.db, 0)).toEqual([]);
56 });
57
58 test("a conflicted pull is left out", () => {
59 const c = pull(5, "2026-11-01T04:00:00.000Z");
60 app.db.query(`INSERT INTO run (id, repo_id, kind, issue_id, state, reason, created_at) VALUES ('0002x', ?1, 'agent', ?2, 'queued', 'conflict', '2026-11-02T00:00:00.000Z')`).run(repoId, c);
61 expect(oldestWaiting(app.db, 10)).toEqual(["trevato/open#1", "trevato/open#3", "trevato/open#4"]);
62 });
63
64 test("a private repository is left out by default, named only when asked", async () => {
65 const o = app.db.query<{ id: string }, []>("SELECT id FROM user WHERE username = 'trevato'").get()!;
66 const r = await createRepo(app.db, app.reposRoot, { ownerId: o.id, name: "secret", visibility: "private", description: "r" });
67 if (!r.ok) throw new Error(r.error);
68 addCrew(app.db, r.value.id, agent, o.id);
69 const issue = id();
70 app.db.query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, 1, 'pull', 't', '', ?3, 'open', '2026-10-01T00:00:00.000Z')`).run(issue, r.value.id, agent);
71 app.db.query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id) VALUES (?1, 'main', 'b', '', 'h', ?2)`).run(issue, r.value.id);
72 expect(oldestWaiting(app.db, 1)).toEqual(["trevato/open#1"]);
73 expect(oldestWaiting(app.db, 1, false)).toEqual(["trevato/secret#1"]);
74 });
75
76 test("the filed stale_pile issue lists the oldest waiting pulls, and names private ones only in a private repository", () => {
77 const was = process.env.FORGE_SENTINEL_REPO;
78 process.env.FORGE_SENTINEL_REPO = "trevato/open";
79 try {
80 app.db.query("UPDATE repo SET issue_seq = 100 WHERE name = 'open'").run();
81 const rate = { lastHour: 0, recent: 0.5, best: 3, ranShare: 1 };
82 const body = (): string => app.db.query<{ body: string }, []>("SELECT body FROM issue WHERE title LIKE 'Throughput:%' ORDER BY created_at DESC, rowid DESC LIMIT 1").get()!.body;
83 fileOnce(app, { cause: "stale_pile", says: "Open crew pulls wait." }, rate, Date.now());
84 expect(body()).toContain("Oldest waiting, start here:\n- trevato/open#1\n- trevato/open#3\n- trevato/open#4");
85 expect(body()).not.toContain("secret");
86 app.db.query("UPDATE repo SET visibility = 'private' WHERE name = 'open'").run();
87 fileOnce(app, { cause: "stale_pile", says: "Open crew pulls wait again." }, rate, Date.now());
88 expect(body()).toContain("- trevato/secret#1");
89 } finally {
90 if (was === undefined) delete process.env.FORGE_SENTINEL_REPO;
91 else process.env.FORGE_SENTINEL_REPO = was;
92 }
93 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs