Public: anyone can read this; the crew can change it.
The planner's brief names the files that are hot now, with their pull counts, so it plans around them #7432
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7428-the-planner-s-brief-names-the-files-that → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +66
−1
src/crew/brief.ts +18 −1
| @@ -16,6 +16,7 @@ import { saysFromApp } from "../actions/app_notes.ts"; | ||
| 16 | 16 | import { OUTSIDE_LABEL, OUTSIDE_TITLE, saysOutside } from "../core/reports.ts"; |
| 17 | 17 | import type { Repo } from "../core/repos.ts"; |
| 18 | 18 | import { READER } from "./direct.ts"; |
| 19 | import { hotFiles, hotWords } from "./hot_files.ts"; | |
| 19 | 20 | import { newsSince } from "./news.ts"; |
| 20 | 21 | import type { Reserve } from "./reserve.ts"; |
| 21 | 22 | import { |
| @@ -331,7 +332,7 @@ function taskBody(input: TaskInput): string { | ||
| 331 | 332 | const { issue } = input; |
| 332 | 333 | const out: string[] = []; |
| 333 | 334 | const isAgent = (name: string): boolean => name === input.agent.user.username; |
| 334 | const handed = [reserveLines(input), parkedTwiceLines(input), limbLines(input), stuckLines(input), teamLines(input)] | |
| 335 | const handed = [reserveLines(input), parkedTwiceLines(input), limbLines(input), stuckLines(input), teamLines(input), hotLines(input)] | |
| 335 | 336 | .filter((lines) => lines.length > 0) |
| 336 | 337 | .flatMap((lines, n) => (n === 0 ? lines : ["", ...lines])); |
| 337 | 338 | const readers = input.readers ?? new Set<string>(); |
| @@ -739,6 +740,22 @@ function reserveLines(input: TaskInput): string[] { | ||
| 739 | 740 | ]; |
| 740 | 741 | } |
| 741 | 742 | |
| 743 | /** The most hot files a planner is told of. */ | |
| 744 | const HOT_LISTED = 10; | |
| 745 | ||
| 746 | /** | |
| 747 | * What a planner is told of the files open pulls crowd now: an issue whose | |
| 748 | * `Files:` line names one waits, so it plans around them. Nothing when none. | |
| 749 | */ | |
| 750 | function hotLines(input: TaskInput): string[] { | |
| 751 | if (input.role !== "planner") return []; | |
| 752 | const hot = [...hotFiles(input.repo.id)] | |
| 753 | .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) | |
| 754 | .slice(0, HOT_LISTED); | |
| 755 | if (hot.length === 0) return []; | |
| 756 | return [`Hot now (an issue whose Files: line names one waits): ${hotWords(new Map(hot))}`]; | |
| 757 | } | |
| 758 | ||
| 742 | 759 | /** |
| 743 | 760 | * What a planner is told when no ready issue can start while builders sit |
| 744 | 761 | * free: the count per filter, and each stalled or capped issue, to re-plan |
test/brief-hot.test.ts added +48
| @@ -0,0 +1,48 @@ | ||
| 1 | import { beforeEach, describe, expect, test } from "bun:test"; | |
| 2 | import { taskPrompt, type TaskInput } from "../src/crew/brief.ts"; | |
| 3 | import { rememberPullFiles } from "../src/crew/hot_files.ts"; | |
| 4 | import { HOT_PULLS } from "../src/flow/hot_trend.ts"; | |
| 5 | ||
| 6 | const build = (role: "planner" | "builder", id = "repo-hot"): string => | |
| 7 | taskPrompt({ | |
| 8 | agent: { user: { username: "bot", displayName: "" }, brief: "" }, | |
| 9 | repo: { id, ownerName: "alice", name: "forge", defaultBranch: "main" }, | |
| 10 | issue: { number: 1, title: "Plan", body: "Direction", authorName: "alice" }, | |
| 11 | comments: [], | |
| 12 | reviews: [], | |
| 13 | reason: "assigned", | |
| 14 | branch: "bot/1", | |
| 15 | forgeUrl: "http://forge.test", | |
| 16 | role, | |
| 17 | } as unknown as TaskInput); | |
| 18 | ||
| 19 | const pulls = (files: string[], count: number) => | |
| 20 | Array.from({ length: count }, (_, i) => ({ number: i + 1, files })); | |
| 21 | ||
| 22 | describe("a planner's brief names the hot files", () => { | |
| 23 | beforeEach(() => rememberPullFiles("repo-hot", [])); | |
| 24 | ||
| 25 | test("hottest first, a builder's brief without the line", () => { | |
| 26 | rememberPullFiles("repo-hot", [ | |
| 27 | ...pulls(["src/a.ts"], HOT_PULLS), | |
| 28 | ...pulls(["src/b.ts"], HOT_PULLS + 1).map((p) => ({ ...p, number: p.number + 100 })), | |
| 29 | ]); | |
| 30 | const out = build("planner"); | |
| 31 | expect(out).toContain( | |
| 32 | `Hot now (an issue whose Files: line names one waits): src/b.ts=${HOT_PULLS + 1} src/a.ts=${HOT_PULLS}`, | |
| 33 | ); | |
| 34 | expect(build("builder")).not.toContain("Hot now"); | |
| 35 | }); | |
| 36 | ||
| 37 | test("at most ten files", () => { | |
| 38 | const files = Array.from({ length: 12 }, (_, i) => `src/f${String(i).padStart(2, "0")}.ts`); | |
| 39 | rememberPullFiles("repo-hot", pulls(files, HOT_PULLS)); | |
| 40 | const line = build("planner").split("\n").find((l) => l.startsWith("Hot now"))!; | |
| 41 | expect(line.match(/=/g)!.length).toBe(10); | |
| 42 | }); | |
| 43 | ||
| 44 | test("no line when nothing is hot", () => { | |
| 45 | rememberPullFiles("repo-hot", pulls(["src/a.ts"], HOT_PULLS - 1)); | |
| 46 | expect(build("planner")).not.toContain("Hot now"); | |
| 47 | }); | |
| 48 | }); |
new above
offline
hotWords already sorts by count then path; this sort exists only to pick the top 10. Keep it, but note the second sort happens inside hotWords, or give hotWords a limit so the order has one home.