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

An issue the crew found done on main is held as done_on_main, not stalled #2516

Merged opened this

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

Merged

scout/forge:scout/2513-an-issue-the-crew-found-done-on-main-is → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/2513-an-issue-the-crew-found-done-on-main-is was merged into main as 937c830.

Review

Sign in to review this change.
4 files changed +44 −6
README.md +2 −1
@@ -822,7 +822,8 @@ the hold; an issue handed to the planner is still owed its pass.
822822 Every tick says why each ready issue a builder cannot start is held, counted
823823 under the first filter that holds it (`crew/held.ts`): `held: running=1
824824 open_pull=10 stalled=12 run_cap=11` — a run on it, an open pull proposing it,
825 the planner's, stalled, at its run cap, parked twice, covered by a red-main
825 the planner's, found done on main and waiting for a person to close it
826 (`done_on_main`, never re-planned), stalled, at its run cap, parked twice, covered by a red-main

This line runs past the wrap width of the lines around it; rewrap the paragraph.

826827 fix, or a setting's finding. `/flow` draws the same per autonomous app as one
827828 bar, and the terminal's Flow command prints it as a line per app
828829 (`flow/held.ts`, kept 5 s). When nothing is startable while builder slots are free and some of the
src/crew/held.ts +5 −1
@@ -9,6 +9,8 @@
99 * - `running`: a run is in flight on it.
1010 * - `open_pull`: an open pull already proposes it (`direct.ts`).
1111 * - `planner`: the planner or a reviewer holds it, not a builder.
12 * - `done_on_main`: the crew found it done on the base branch and waits for
13 * a person to close it (`done_on_main.ts`); nothing is left to plan.
1214 * - `stalled`: a builder ran on it and left without a pull, and nobody
1315 * who directs the crew has spoken since.
1416 * - `run_cap`: it has used its runs for the day (`guard.ts`).
@@ -23,7 +25,7 @@
2325 import type { App } from "../app.ts";
2426 import { listCrewRoles, type Agent, type CrewRole } from "../core/agents.ts";
2527 import { readyNow, repoGraph, type Ranked } from "../core/dependencies.ts";
26 import { BY_CREW, DONE_LIKE } from "../core/done_on_main.ts";
28 import { alreadyDone, BY_CREW, DONE_LIKE } from "../core/done_on_main.ts";
2729 import type { Issue } from "../core/issues.ts";
2830 import type { Repo } from "../core/repos.ts";
2931 import { listActive, type Run } from "../core/runs.ts";
@@ -40,6 +42,7 @@ export const HOLDS = [
4042 "running",
4143 "open_pull",
4244 "planner",
45 "done_on_main",

New hold name: every HOLDS consumer needs a matching style. src/web/pages/flow.ts has a .held-<name> fill per hold, so add .held-done_on_main there.

4346 "stalled",
4447 "run_cap",
4548 "parked_twice",
@@ -116,6 +119,7 @@ export function heldBy(
116119 if (busyIssues.has(issue.id)) return "running";
117120 if (proposed.has(issue.number)) return "open_pull";
118121 if (issue.assigneeId !== null && planners.has(issue.assigneeId)) return "planner";
122 if (alreadyDone(app.db, [issue.id]).has(issue.id)) return "done_on_main";
119123 if (stalled(app, repo, issue)) return "stalled";
120124 if (atRunCap(app, repo, issue)) return "run_cap";
121125 if (parkedCount(app.db, repo, issue.id) >= PARK_STRIKES) return "parked_twice";
src/web/pages/flow.ts +1
@@ -72,6 +72,7 @@ const css = /* css */ `
7272 .held-running { fill: var(--accent); }
7373 .held-open_pull { fill: var(--merged); }
7474 .held-planner { fill: var(--primary); }
75 .held-done_on_main { fill: var(--add-fg); }
7576 .held-stalled { fill: var(--attn); }
7677 .held-run_cap { fill: var(--danger); }
7778 .held-parked_twice { fill: var(--del-fg); }
test/crew-held.test.ts +36 −4
@@ -18,12 +18,13 @@ import { commitToBranch, createRepo, updateRepoSettings } from "../src/actions/r
1818 import { getAgentByName, setCrewRole } from "../src/core/agents.ts";
1919 import { getIssueById, type Issue } from "../src/core/issues.ts";
2020 import { getRepoById, type Repo } from "../src/core/repos.ts";
21 import { DONE_NOTE } from "../src/core/done_on_main.ts";
2122 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
2223 import { createUser, getUserByName, type User } from "../src/core/users.ts";
2324 import { driveRepo } from "../src/crew/autonomy.ts";
2425 import { taskPrompt } from "../src/crew/brief.ts";
2526 import { ISSUE_RUN_CAP } from "../src/crew/guard.ts";
26 import { heldCounts, heldNow, heldWords, stuckOf } from "../src/crew/held.ts";
27 import { heldCounts, heldNow, heldWords, replannable, stuckOf } from "../src/crew/held.ts";
2728 import { PARK_NOTE } from "../src/crew/parks.ts";
2829 import { keyPart } from "../src/crew/sentinel.ts";
2930 import { HELD_TTL_MS, readHeld } from "../src/flow/held.ts";
@@ -145,6 +146,9 @@ describe("the tick says why each ready issue cannot start", () => {
145146 const planned = issue(repo, "The planner's");
146147 expect(assignIssue(app, alice, repo, planned, atlas).ok).toBe(true);
147148
149 const done = issue(repo, "Done already");
150 addComment(app, mason, repo, done, `${DONE_NOTE}${"b".repeat(40)}`);
151
148152 const stuck = issue(repo, "Stalled");
149153 ran(repo, stuck, mason, "declined: the ask is unclear");
150154
@@ -185,6 +189,7 @@ describe("the tick says why each ready issue cannot start", () => {
185189 expect(named("running")).toEqual([running.number]);
186190 expect(named("open_pull")).toEqual([proposed.number]);
187191 expect(named("planner")).toEqual([planned.number]);
192 expect(named("done_on_main")).toEqual([done.number]);
188193 expect(named("stalled")).toEqual([stuck.number]);
189194 expect(named("run_cap")).toEqual([capped.number]);
190195 expect(named("parked_twice")).toEqual([parked.number]);
@@ -192,12 +197,32 @@ describe("the tick says why each ready issue cannot start", () => {
192197 expect(named("setting")).toEqual([finding.number]);
193198 expect(heldCounts(held).open_pull).toBe(1);
194199 expect(heldWords(held)).toBe(
195 "running=1 open_pull=1 planner=1 stalled=1 run_cap=1 parked_twice=1 red_fix=1 setting=1",
200 "running=1 open_pull=1 planner=1 done_on_main=1 stalled=1 run_cap=1 parked_twice=1 red_fix=1 setting=1",
196201 );
197202 // Something can start: the planner is not told the crew is stuck.
198203 expect(stuckOf(held)).toBeUndefined();
199204 });
200205
206 test("an issue found done on main is held as done_on_main, never re-planned, until the crew parks it", async () => {
207 const { repo, mason } = await crewed(app, alice, "closing");
208 const fresh = () => getRepoById(app.db, repo.id)!;
209 const done = issue(fresh(), "Already shipped");
210 addComment(app, mason, fresh(), done, `${DONE_NOTE}${"c".repeat(40)}`);
211 const held = heldNow(app, fresh());
212 expect(held.by.done_on_main.map((i) => i.number)).toEqual([done.number]);
213 expect(held.by.stalled).toHaveLength(0);
214 expect(replannable(held)).toHaveLength(0);
215 expect(heldWords(held)).toBe("done_on_main=1");
216 expect(stuckOf(held)).toBeUndefined();
217
218 addComment(app, mason, fresh(), getIssueById(app.db, done.id)!, `${PARK_NOTE}not done after all`);
219 // The builder that tried it since left without a pull: stalled, so the planner's.
220 ran(fresh(), done, mason, "declined: it is not there", 0);
221 const again = heldNow(app, fresh());
222 expect(again.by.done_on_main).toHaveLength(0);
223 expect(replannable(again).map((i) => i.number)).toEqual([done.number]);
224 });
225
201226 test("nothing startable with a slot free asks the planner once, with the breakdown, whatever the plate", async () => {
202227 const { repo, atlas, mason } = await crewed(app, alice, "stuck");
203228 const fresh = () => getRepoById(app.db, repo.id)!;
@@ -272,6 +297,9 @@ describe("the tick says why each ready issue cannot start", () => {
272297 const shut = await crewed(app, alice, "shut-door");
273298 for (const { repo, mason } of [open, shut]) {
274299 ran(repo, issue(repo, "Stalled"), mason, "declined: too big", 10);
300 if (repo === open.repo) {
301 addComment(app, mason, repo, issue(repo, "Shipped"), `${DONE_NOTE}${"d".repeat(40)}`);
302 }
275303 const capped = issue(repo, "Capped");
276304 for (let n = 0; n < ISSUE_RUN_CAP; n++) ran(repo, capped, mason, `opened pull #${90 + n}`, 20 - n, "ok");
277305 }
@@ -290,6 +318,10 @@ describe("the tick says why each ready issue cannot start", () => {
290318 expect(row).toContain('class="held-bar"');
291319 expect(row).toContain('<span class="mono">startable=0</span>');
292320 expect(row).toContain('<span class="mono">stalled=1</span>');
321 expect(row).toContain('<span class="mono">done_on_main=1</span>');
322 expect(row).toContain('data-held="done_on_main"');
323 expect(row).toContain('<rect class="held-done_on_main"');
324 expect(page).toContain(".held-done_on_main { fill:");
293325 expect(row).toContain('<span class="mono">run_cap=1</span>');
294326 expect(page).toContain('data-held-repo="alice/shut-door"');
295327
@@ -297,10 +329,10 @@ describe("the tick says why each ready issue cannot start", () => {
297329 // reader the public one alone.
298330 const owner = flowLines(app.db, new Date(), [], undefined, readHeld(app, true)).join("\n");
299331 expect(owner).toContain("Ready, and why it waits:");
300 expect(owner).toContain("alice/open-door startable=0 stalled=1 run_cap=1");
332 expect(owner).toContain("alice/open-door startable=0 done_on_main=1 stalled=1");
301333 expect(owner).toContain("alice/shut-door startable=0 stalled=1 run_cap=1");
302334 const agent = flowLines(app.db, new Date(), [], { queues: [] }, readHeld(app, false)).join("\n");
303 expect(agent).toContain("alice/open-door startable=0 stalled=1 run_cap=1");
335 expect(agent).toContain("alice/open-door startable=0 done_on_main=1 stalled=1");
304336 expect(agent).not.toContain("shut-door");
305337 expect(heldLines([], 58)).toEqual(["Ready, and why it waits: no repository runs on its own."]);
306338 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs