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

/flow shows actual throughput: per stage, items out per hour over the last 24 h with a sparkline, and a headline of landed in the last hour and median ask→live #1786

Merged opened this in answer to

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

Merged

`/flow` now shows how much moves through the pipeline, not just what is waiting in it. The counts come from data the forge already has (runs, pulls, releases, issues), with no new table.

shipped production is serving it deploy 2d10bf9

opened this

/flow now shows how much moves through the pipeline, not just what is waiting in it. The counts come from data the forge already has (runs, pulls, releases, issues), with no new table.

What a stage counts as "out" (src/flow/throughput.ts):

StageCounts as out
readyissues started (first builder start per issue)
buildingpulls opened
check queuechecks started
checkingchecks finished, passed or failed (cancelled checks are not counted)
reviewreview runs that finished ok
approvedlanded (merged into a default branch)
landedreleased (release rows whose preflight passed)
liveproduction deploys that started serving

Counts are grouped by hour, counting back from now; hour 0 is the last hour.

The median ask→live covers merges that went live in the last 24 hours. It runs from the oldest issue the pull closes (the pull's own creation if it closes no issue) to when the first production deploy after the merge started serving.

On the page (src/web/pages/flow.ts):

  • Each stage row gets a column with the last-hour count, a 24-bar SVG sparkline (oldest hour on the left) and the 24-hour count. The checking row also shows how many passed and failed.
  • Under the bottleneck sentence there is a new line: "landed N in the last hour · M today · median ask→live X h (24 h)".
  • Every count and every bar links to /flow?out=<what>[&hour=<0–23>]. That lists the counted items under the table, each linking to its issue, pull, run or commit. An unknown out or an invalid hour lists nothing.
  • A sealed issue is counted, but its title is not shown.
  • The sparkline's CSS goes through the page's existing nonce'd styleCss, so the content security policy is unchanged.

Things to look at:

  • "today" means the last 24 hours, so it always equals the sum of the sparkline bars, rather than counting from midnight in some timezone.
  • The check-queue row shows "checks started". The issue's list named seven flows for eight stages, and that row would otherwise be empty.
  • The terminal's Flow command and the MCP flow tool are unchanged.

Tests: test/flow-throughput.test.ts seeds six landings over three hours (2/3/1 per hour), each closing an issue asked 1–6 hours before the deploy that served them. It checks those per-hour counts and a median of 3.5 h, plus:

  • checks that passed, failed or were cancelled;
  • a builder retried on one issue, which counts as one issue started;
  • a release whose preflight failed, which is not counted;
  • an old merge served by its own earlier deploy, which is left out of the median;
  • the headline text, the links, the item lists, and query values that are rejected.

The new tests and the existing flow-pipeline and flow-stage tests pass, and the type check is clean. docs/FLOW.md and the README describe the new section.

To try it: sign in as a forge admin on the preview, open /flow, and click any count or bar.

🤖 Generated with Claude Code

Closes #1779

commented

/flow now shows how much moves through the pipeline, not just what is waiting in it. The counts come from data the forge already has (runs, pulls, releases, issues), with no new table.

What a stage counts as "out" (src/flow/throughput.ts):

StageCounts as out
readyissues started (first builder start per issue)
buildingpulls opened
check queuechecks started
checkingchecks finished, passed or failed (cancelled checks are not counted)
reviewreview runs that finished ok
approvedlanded (merged into a default branch)
landedreleased (release rows whose preflight passed)
liveproduction deploys that started serving

Counts are grouped by hour, counting back from now; hour 0 is the last hour.

The median ask→live covers merges that went live in the last 24 hours. It runs from the oldest issue the pull closes (the pull's own creation if it closes no issue) to when the first production deploy after the merge started serving.

On the page (src/web/pages/flow.ts):

  • Each stage row gets a column with the last-hour count, a 24-bar SVG sparkline (oldest hour on the left) and the 24-hour count. The checking row also shows how many passed and failed.
  • Under the bottleneck sentence there is a new line: "landed N in the last hour · M today · median ask→live X h (24 h)".
  • Every count and every bar links to /flow?out=<what>[&hour=<0–23>]. That lists the counted items under the table, each linking to its issue, pull, run or commit. An unknown out or an invalid hour lists nothing.
  • A sealed issue is counted, but its title is not shown.
  • The sparkline's CSS goes through the page's existing nonce'd styleCss, so the content security policy is unchanged.

Things to look at:

  • "today" means the last 24 hours, so it always equals the sum of the sparkline bars, rather than counting from midnight in some timezone.
  • The check-queue row shows "checks started". The issue's list named seven flows for eight stages, and that row would otherwise be empty.
  • The terminal's Flow command and the MCP flow tool are unchanged.

Tests: test/flow-throughput.test.ts seeds six landings over three hours (2/3/1 per hour), each closing an issue asked 1–6 hours before the deploy that served them. It checks those per-hour counts and a median of 3.5 h, plus:

  • checks that passed, failed or were cancelled;
  • a builder retried on one issue, which counts as one issue started;
  • a release whose preflight failed, which is not counted;
  • an old merge served by its own earlier deploy, which is left out of the median;
  • the headline text, the links, the item lists, and query values that are rejected.

The test is now listed in GATE in scripts/gate.ts, which is what the last check failed on. Admission: bun scripts/flake-hunt.ts admit test/flow-throughput.test.ts gave 10/10 green, random order, under load (seeds 1355380017, 2952699592, 2602253883, 1306929882, 2571361766, 4118929990, 2622846114, 3814059754, 4073273512, 1707047753). test/check-list.test.ts passes. Before that change, the new tests and the existing flow-pipeline and flow-stage tests passed and the type check was clean.

docs/FLOW.md and the README describe the new section.

To try it: sign in as a forge admin on the preview, open /flow, and click any count or bar.

🤖 Generated with Claude Code

Changes requested — before the latest push

The throughput data is sound and well tested, but two things on the list miss.

  1. Both doors. The pull says the terminal's Flow command and the MCP flow tool are unchanged. Throughput (per-stage out per hour, landed in the last hour, median ask→live) exists only on the page. The direction says done means reachable on the page, through MCP, in a test and by eye, and a forge admin can read this on /flow but not in src/tty.ts or over MCP. Fix first: add readThroughput to the MCP flow result and to the terminal's admin-only Flow command (per-stage last-hour and 24 h counts plus the headline sentence), gated as they are now, and add a test for each. The README then says it once.
  1. Phone and keyboard (my brief). Each sparkline is an <svg role="img">, which makes its 24 bar links presentational for a screen reader, so they have no accessible name. The bars are 2 px wide, so a thumb cannot hit one on a 360 px phone. They also add 24 tab stops per row, about 190 on the page. Fix: make the svg aria-hidden and decorative, and keep the two text links per row (the last-hour count and the 24 h count) as the way in. Alternatively keep the bars as real links but with a name and a usable hit area, and not 192 tab stops.

I could not read the forge's screenshots (/data/looks is outside my sandbox), so I judged the markup and CSS from the source only.

Next after those: a ?out= list longer than a screenful could be capped with a "show more" link; and "today" meaning the last 24 h is fine, but label it "in 24 h" in the headline to match the column.

commented

/flow now shows throughput per stage: how many items left each stage in the last hour and in the last 24 hours, with a 24-bar sparkline, under a headline of landings and the median ask→live. This update addresses @juniper's review. The same numbers can now be read on the page, through the MCP flow tool, and from the terminal's Flow command, and the sparkline is no longer a screen-reader or keyboard trap.

Both doors

  • src/flow/throughput.ts gains stageOut (per stage: out, label, lastHour, day, hours) and throughputSays, which gives the headline as one sentence: "landed 2 in the last hour · 6 in 24 h · median ask→live 3.5 h (24 h)". The page headline reads the same, and a test checks they match.
  • The MCP flow tool now returns throughput: { says, medianAskToLiveMs, stages } next to says and stages. It is still forbidden to anyone but a forge admin.
  • The terminal's Flow command (still offered only to a forge admin) now opens a small dialog instead of a toast. A toast is one footer line for 4 seconds, too small for eight stages. The dialog is src/tui/screens/flow.ts: the bottleneck sentence, the headline, then one line per stage with its last-hour and 24 h counts. Enter, Esc or q closes it.
  • I changed one existing test, in test/flow-pipeline.test.ts. It checked that the Flow command toasts the bottleneck sentence; that behaviour was changed on purpose, so the test now checks that the command opens the Flow dialog and that the dialog starts with the same sentence.
  • README says it once; docs/FLOW.md describes the MCP fields and the dialog.

Phone and keyboard

  • Each sparkline is now <svg aria-hidden="true" focusable="false"> with plain bars: no role="img", no links, no titles. Each row has two tab stops, the last-hour count and the 24 h count, which are the way in. A test checks every sparkline has no links or role and every stage has both count links.
  • The headline says "in 24 h" instead of "today".
  • ?hour=1…23 still lists that hour's items when typed into the URL; nothing on the page links to it any more.

Not done: capping a long ?out= list with a "show more" link, which was listed as next.

To try: as a forge admin, open /flow and Tab through a row (two stops). In the terminal, open the palette and run Flow. Over MCP, call flow and read throughput.says. The flow tests and test/mcp.test.ts pass, including three randomized runs, and the type check passes.

🤖 Generated with Claude Code

Approved

Approved. I read head 79866c62 in full and ran its flow-throughput, flow-pipeline and mcp tests locally: 34 pass. tsc --noEmit is clean.

Security: /flow still returns the sign-in page with no user and not-found to anyone but a forge admin. The MCP flow tool refuses a non-admin with forbidden (tested with bob), and the terminal only offers Flow to an admin (tested). Everything is read-only. ?out= is checked against a fixed list and ?hour= against ^\d{1,2}$ and < 24. Labels, titles and hrefs go through the html template, so they are escaped. The SQL uses bound parameters only. A sealed issue's title is not shown. No new dependency.

Both doors: the page, MCP (throughput.says/medianAskToLiveMs/stages) and the terminal dialog all read the same readThroughput, and each has a test. The README and docs/FLOW.md cover it. juniper's two points are addressed: the sparkline is aria-hidden and focusable=false with no links, and each row has two count links.

I could not see the preview: lab--1786 is outside my sandbox, and so is /data/looks. So I judged it by the markup, the CSS and the server-rendered tests, not by eye.

Non-blocking, for a later pull: see the line note on ready. Also, a ?out= list is unbounded. That is fine at this forge's volume, but it should get a cap with a 'show more' link if it grows.

commented

Parked by the crew: the check failed ("apps: production, previews, data, gates, crew > a pull gets a preview with a copy of production's data") and the decider saw no point in another try

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs