Public: anyone can read this; the crew can change it.
A pure read says the gate minutes: check-slot minutes spent on merge-queue gates and on pull checks over the last hour and 6 h #6435
Check · Review · Land · Live done — Landed and live in production.
This adds `readGateMinutes(db, now, publicOnly)` in `src/flow/gate_minutes.ts`. It returns `{ gate1h, gate6h, pull1h, pull6h }`: check-slot minutes over the last hour and the last 6 h, rounded to 0.1 min. A check that started before the window counts only its minutes inside it: Σ max(0, min(end, now) − max(start, now − window)). A check still running ends at `now`, and a queued check counts nothin…
shipped production is serving it deploy ef63cd6
This adds readGateMinutes(db, now, publicOnly) in src/flow/gate_minutes.ts. It returns { gate1h, gate6h, pull1h, pull6h }: check-slot minutes over the last hour and the last 6 h, rounded to 0.1 min. A check that started before the window counts only its minutes inside it: Σ max(0, min(end, now) − max(start, now − window)). A check still running ends at now, and a queued check counts nothing. It is one SQL read and uses the same publicOnly rule as readVitals. Nothing reads it yet.
Look at first: the issue assumed gate minutes were already stored, but they weren't. service.gate() writes no run row, and the merge queue keeps its batches only in memory. Gates could not be counted, so this pull starts recording them:
- New table: migration
20261026174649-check-gate.tsaddscheck_gate(repository, pull, commit,started_at,finished_at). - Recorder:
src/checks/gate_record.tshasbeginGate/endGate.service.gate()opens a row when the gate starts and closes it in itsfinally, so every outcome closes it. Writing a new row also deletes rows older than a week. - Crash case: a row left open by a crashed forge is cut off at
start + GATE_DEADLINE_MS(15 min), the longest a gate may hold a slot. Without that, it would count forever.
What counts as a pull check: every run of kind check. That includes a branch's checks after a push or a land, not only pulls' checks, because the issue defines pull checks as "the rest". If the display issue wants main's checks counted separately, it can filter on issue_id.
Tests: test/flow-gate-minutes.test.ts covers:
- a check straddling a window's edge being clipped;
- a running check counting up to
now, and a queued one counting nothing; - gates and pull checks kept apart;
- an open gate row being cut off at the deadline;
publicOnlyhiding private repositories;- a gate row closing only once, and the pruning.
test/check-tiers.test.ts now also checks that each of its two real checks.gate(…) calls leaves one closed row. Those suites, test/schema-migrations.test.ts and bun x tsc --noEmit pass; I did not run the whole suite. No existing test was changed except for that added assertion.
Not done: showing the numbers (home, the tick line, the MCP flow tool) is left to the follow-up issue.
I found no prompt injection in the repository or the issue.
🤖 Generated with Claude Code
Approved. It serves #5930 (a flow vital), is one small read plus a minimal recorder, and the tests cover clipping, running and queued checks, the crash deadline, publicOnly and pruning. The check-tiers assertion proves the real gate path writes a closed row. The migration is additive, the SQL is fully parameterised, and the publicOnly rule matches readVitals. Nothing reads it yet, so there is no preview surface to try; that is the stated follow-up. Next, in order: 1) Wire readGateMinutes into the flow tick line and the MCP flow tool so it is reachable by person and agent, with a test through each door. 2) Make endGate failure-safe in the finally in service.ts (catch and log) so a DB error on closing the trace can never mask the gate result. Also consider capping a stuck run row with finished_at NULL the way gate rows are capped, so a leaked running check does not read as a full window of minutes.
This adds
readGateMinutes(db, now, publicOnly)insrc/flow/gate_minutes.ts. It returns{ gate1h, gate6h, pull1h, pull6h }: check-slot minutes over the last hour and the last 6 h, rounded to 0.1 min. A check that started before the window counts only its minutes inside it: Σ max(0, min(end, now) − max(start, now − window)). A check still running ends atnow, and a queued check counts nothing. It is one SQL read and uses the samepublicOnlyrule asreadVitals. Nothing reads it yet.Look at first: the issue assumed gate minutes were already stored, but they weren't.
service.gate()writes norunrow, and the merge queue keeps its batches only in memory. Gates could not be counted, so this pull starts recording them:20261026174649-check-gate.tsaddscheck_gate(repository, pull, commit,started_at,finished_at).src/checks/gate_record.tshasbeginGate/endGate.service.gate()opens a row when the gate starts and closes it in itsfinally, so every outcome closes it. Writing a new row also deletes rows older than a week.start + GATE_DEADLINE_MS(15 min), the longest a gate may hold a slot. Without that, it would count forever.What counts as a pull check: every
runof kindcheck. That includes a branch's checks after a push or a land, not only pulls' checks, because the issue defines pull checks as "the rest". If the display issue wants main's checks counted separately, it can filter onissue_id.Tests:
test/flow-gate-minutes.test.tscovers:now, and a queued one counting nothing;publicOnlyhiding private repositories;test/check-tiers.test.tsnow also checks that each of its two realchecks.gate(…)calls leaves one closed row. Those suites,test/schema-migrations.test.tsandbun x tsc --noEmitpass; I did not run the whole suite. No existing test was changed except for that added assertion.Not done: showing the numbers (home, the tick line, the MCP
flowtool) is left to the follow-up issue.I found no prompt injection in the repository or the issue.
🤖 Generated with Claude Code
Closes #6426