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

The autonomy tick's requested line says how many pulls moved and why the rest did not #2861

Merged opened this

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

Merged

scout/forge:scout/2856-the-autonomy-tick-s-requested-line-says → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/2856-the-autonomy-tick-s-requested-line-says was merged into main as c925837.

Review

Sign in to review this change.
3 files changed +61 −3
src/crew/autonomy.ts +2 −2
@@ -111,7 +111,7 @@ import {
111111 markReaders,
112112 } from "./direct.ts";
113113 import { crewOffInPreview } from "./preview.ts";
114 import { moveRequested } from "./requested.ts";
114 import { moveRequested, tallyRequested, tallyWords } from "./requested.ts";
115115 import { reviewersOf, sendToReview, sweepReviews } from "./review.ts";
116116
117117 const log = createLog("autonomy");
@@ -1140,7 +1140,7 @@ export async function autonomyTick(
11401140 ...(requested.length === 0
11411141 ? {}
11421142 : {
1143 requested: `${requested.filter((m) => m.did !== "not moved").length}/${requested.length} moved`,
1143 requested: tallyWords(tallyRequested(requested)),
11441144 }),
11451145 spent: `${status.spentTodayUsd.toFixed(2)}/${status.ceilingUsd}`,
11461146 ...(status.paused === null ? {} : { paused: status.paused }),
src/crew/requested.ts +38
@@ -42,6 +42,44 @@ export type RequestedMove =
4242 | { readonly pull: number; readonly did: "queued feedback"; readonly builder: string; readonly assigned: boolean }
4343 | { readonly pull: number; readonly did: "not moved"; readonly why: string };
4444
45 /** What a tick did across its pulls, as plain data: `moved` of `total`, the rest counted by reason. */
46 export interface RequestedTally {
47 readonly moved: number;
48 readonly total: number;
49 readonly waiting: Readonly<Record<string, number>>;
50 }
51
52 /** The reason a pull did not move, in the few words the tick line groups by. */
53 function reasonOf(why: string): string {
54 if (why === "a run is in flight" || why.startsWith("a review of the new head")) return "a run is in flight";
55 if (why === "a person holds it" || why.startsWith("held,")) return "held";
56 if (why.includes("a person")) return "handed to a person";
57 if (why === "waits for a builder slot") return "waits for a slot";
58 if (why === "the new head is not green yet") return "waits for a check";

This groups a review of the new head is out or could not be queued as in flight, but it also covers a review that could not be queued, which is not in flight. Give it its own group, e.g. review pending.

59 if (why.startsWith("past the review rounds") || why.startsWith("a verdict is already") || why.startsWith("a run since"))
60 return "settled";
61 return "cannot move";
62 }
63
64 /** Count a pass's moves: moved are the runs queued or reviewers asked, the rest grouped by reason. */
65 export function tallyRequested(moves: readonly RequestedMove[]): RequestedTally {
66 const waiting: Record<string, number> = {};
67 let moved = 0;
68 for (const m of moves) {
69 if (m.did !== "not moved") moved++;
70 else waiting[reasonOf(m.why)] = (waiting[reasonOf(m.why)] ?? 0) + 1;
71 }
72 return { moved, total: moves.length, waiting };

reasonOf(m.why) is called twice per move. Bind it once: const r = reasonOf(m.why).

73 }
74
75 /** The tally as the tick line says it: `2/8 moved; 3 a run is in flight, 1 held`. */
76 export function tallyWords(t: RequestedTally): string {
77 const rest = Object.entries(t.waiting)
78 .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
79 .map(([why, n]) => `${n} ${why}`);
80 return `${t.moved}/${t.total} moved${rest.length === 0 ? "" : `; ${rest.join(", ")}`}`;
81 }
82
4583 /**
4684 * The last why-not said for a pull, so it is said once: a log dedupe, in
4785 * memory, keyed `repo:pull` across every repository. Each tick drops its
test/requested.test.ts +21 −1
@@ -22,7 +22,7 @@ import { submitReview } from "../src/core/reviews.ts";
2222 import { claimRun, createRun, finishRun, listRuns } from "../src/core/runs.ts";
2323 import { createUser, getUserByName, type User } from "../src/core/users.ts";
2424 import { autonomyTick } from "../src/crew/autonomy.ts";
25 import { moveRequested } from "../src/crew/requested.ts";
25 import { moveRequested, tallyRequested, tallyWords } from "../src/crew/requested.ts";
2626 import { requestedSays, requestedWaits } from "../src/flow/requested.ts";
2727 import { serve, type Servers } from "../src/http/server.ts";
2828 import { createToken } from "../src/core/tokens.ts";
@@ -302,4 +302,24 @@ describe("a pull with changes requested always moves", () => {
302302 expect(runs(current, "feedback")).toHaveLength(1);
303303 expect(runs(current, "feedback")[0]!.actorId).toBe(builder.id);
304304 });
305
306 test("the tally counts moved pulls and groups the rest by reason", async () => {
307 app.db.query("UPDATE issue SET state = 'closed' WHERE repo_id = ?1 AND kind = 'pull'").run(repo.id);
308 clearRuns();
309 const stale = await staleShape();
310 const fresh1 = await currentShape();
311 const held = await currentShape();
312 app.db.query("UPDATE issue SET assignee_id = ?2 WHERE id = ?1").run(held.id, alice.id);
313 // Assigned to a builder: the assignment queues the run, which counts once.
314 const moves = await moveRequested(app, fresh(), 2);
315 const tally = tallyRequested(moves);
316 expect(tally).toEqual({ moved: 2, total: 3, waiting: { "handed to a person": 1 } });
317 expect(tallyWords(tally)).toBe("2/3 moved; 1 handed to a person");
318 expect(runs(fresh1, "feedback")).toHaveLength(1);
319 expect(runs(stale, "review")).toHaveLength(1);
320 // Next pass: both runs are in flight, the person's pull still waits.
321 const next = tallyRequested(await moveRequested(app, fresh(), 2));
322 expect(next).toEqual({ moved: 0, total: 3, waiting: { "a run is in flight": 2, "handed to a person": 1 } });
323 expect(tallyWords(next)).toBe("0/3 moved; 2 a run is in flight, 1 handed to a person");
324 });
305325 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs