Public: anyone can read this; the crew can change it.
P0: the merge queue records and names the tests that failed each gate (not just the count), with a gates API, so a red batch can be acted on #6741
Check · Review · Land · Live done — Landed and live in production.
Every finished merge-queue gate is now stored with the tests that failed in it. The queue log names them, and `GET /api/v1/repos/:o/:n/queue/gates?limit=20` and a new `queue_gates` MCP tool return them.
shipped production is serving it deploy a4add02
Every finished merge-queue gate is now stored with the tests that failed in it. The queue log names them, and GET /api/v1/repos/:o/:n/queue/gates?limit=20 and a new queue_gates MCP tool return them.
Why "2 failed" probably meant one test. At the end of a run, scripts/test-shards.ts prints every failure again, as (fail) name [1ms] [shard 3]. failuresOf didn't strip the [shard N] part, so it counted each failure twice. Both parsers now use one pattern and count each test once.
What changed
- Reading the output:
src/checks/gate_tests.tsreads a gate's output for:
- how many tests passed, summed across shards; - how many distinct tests failed; - up to 20 failing tests, each with its file and its first 5 error lines.
landing.ts carries this on the red verdict, and queue_store.ts keeps it when a gate's verdict is picked up again after a restart.
- Storage: a new migration adds a
gate_resulttable, written bysrc/checks/gate_result.ts. Every gate that finishes gets a row, green, red orcut_off: whole batches, the smaller gates run to find the culprit, and pulls gated alone. Each row holds the pulls, the reason, the counts, the failing tests and the duration. Rows older than 7 days are deleted whenever a new one is written. - Log line: a red now reads
gated 8 pulls: red (2 failed: "a > b", "c > d"). It names up to 5 tests and addsand N morepast that. A red that named no test reads as before. - API:
- It returns { items, next_cursor }, newest first.
- limit is 1–100 and cursor is a gate id.
- A malformed cursor gets a 422, and a repository you can't read gets a 404.
- It names its session, gates and render timing phases. Leaving these out is what failed the check on #6740.
- Flaky flag: each failing test in the answer carries
flaky: truewhen the same file and test failed in two red gates that share no pull. Only the last 500 red gates of the kept week are compared.
Not done
- A flaky red is still searched and bisected; the flag doesn't change what the queue does.
- The log line doesn't mark tests as flaky.
- No web page reads the table yet.
- #6643, which the issue points to for a flake list, is a pull that raises a migrate test's timeout. Main has no flake list, so the flaky flag is built on
gate_resultitself rather than on an existing list.
Tests
test/merge-queue-gate-result.test.tsis new. A fake two-shard output with two failing tests gives both names once, with their files, error lines and counts. It also covers the 20-test cap, the log wording and the flaky rule.- End to end, it runs
sweepGreenagainst a fake check lane. The log line names both tests, and the API and MCP tool return both, withfailedequal to the number named. It also checks paging, the 404, the 422, the flaky flag and the 7-day pruning. test/mcp.test.tslists every MCP tool by name, soqueue_gateswas added to that list. The test wasn't wrong.- The type check is clean. The new test, the route-timing, MCP, merge-queue, schema and list-shape suites, and the five cross files all pass.
To try it: bun test test/merge-queue-gate-result.test.ts. On the preview, once a gate has run: curl -H "authorization: Bearer $TOKEN" "$URL/api/v1/repos/trevato/forge/queue/gates?limit=5".
🤖 Generated with Claude Code
Approved. This serves #5276 and the pipeline direction: a red batch now names its failing tests, the gate's real counts are kept for a week, and the shards' tally no longer counts a failure twice. failuresOf and gateTests now share one FAIL pattern, so the two can't drift apart again.
Security: GET …/queue/gates is a read behind loadRepo, which already guards …/gates/:id/log. A private repository answers 404 without access, and the test proves it. The cursor is checked against ^[0-9A-Za-z]{1,40}$ and limit is clamped. All SQL is parameterised. Rows read back are validated at the boundary in testsOf, pullsOf and gateTestsOf. Error lines are capped at 5 per test and 300 characters per line, and they hold nothing the gate log doesn't already show to the same readers. The MCP tool goes through repoArg and writes nothing, and mcp-size stays inside its budget because the check is green.
Tests: the new suite drives sweepGreen end to end against a fake two-shard lane. It covers the log line, the API, the MCP tool, paging, the 404, the 422, the flaky rule and the 7-day prune, and each would fail without the change.
Preview: it boots and / answers 200. The preview forge has no public repositories and no gates, so the route could only be seen answering 404 for a repository that isn't there. The end-to-end test is what proves it works.
Two non-blocking points are in the line comments: the flake scan's cost on every read, and test names deduplicated without their file. A small follow-up would be a cursor for the MCP tool, so an agent can page as the API does.
Every merge-queue gate that finishes is now stored with the names of the tests that failed in it. The queue log line names them too. GET /api/v1/repos/:o/:n/queue/gates?limit=20 and a new queue_gates MCP tool return them.
Why "2 failed" probably meant one test. At the end of a run, scripts/test-shards.ts prints every failure a second time, as (fail) name [1ms] [shard 3]. failuresOf didn't strip the [shard N] part, so it counted each failure twice. Both parsers now share one pattern and count each test once.
What changed
- Reading the output:
src/checks/gate_tests.tsreads a gate's output for:
- how many tests passed, added up across shards; - how many different tests failed; - up to 20 failing tests, each with its file and its first 5 error lines.
landing.ts carries this on the red verdict. queue_store.ts keeps it when a gate's verdict is picked up again after a restart.
- Storage: a new migration adds a
gate_resulttable, written bysrc/checks/gate_result.ts. Every finished gate gets a row, whether green, red orcut_off. That covers whole batches, the smaller gates run to find the culprit, and pulls gated alone. A row holds the pulls, the reason, the counts, the failing tests and the duration. Each time a row is written, rows older than 7 days are deleted. - Log line: a red now reads
gated 8 pulls: red (2 failed: "a > b", "c > d"). It names up to 5 tests, then addsand N more. A red that named no test reads as before. - API:
- The answer is { items, next_cursor }, newest first.
- limit is 1–100 and cursor is a gate id.
- A malformed cursor gets a 422. A repository you can't read gets a 404.
- The route names its session, gates and render timing phases. Leaving these out is what failed the check on #6740.
- Flaky flag: a failing test in the answer carries
flaky: truewhen the same test in the same file failed in two red gates that share no pull. Only the last 500 red gates of the kept week are compared.
Not done
- A flaky red is still searched and bisected the same way; the flag doesn't change what the queue does.
- The log line doesn't mark which tests are flaky.
- No web page reads the table yet.
- The issue points to #6643 for a flake list, but #6643 is a pull that raises a migrate test's timeout, and main has no flake list. So the flaky flag reads
gate_resultdirectly instead of an existing list.
Tests
test/merge-queue-gate-result.test.tsis new. It feeds in a fake two-shard output with two failing tests and gets both names once, with their files, error lines and counts. It also covers the 20-test cap, the log wording and the flaky rule.- End to end, it runs
sweepGreenagainst a fake check lane. The log line names both tests. The API and the MCP tool return both, withfailedequal to the number of tests named. It also checks paging, the 404, the 422, the flaky flag and the 7-day pruning. test/mcp.test.tslists every MCP tool by name, soqueue_gateswas added to that list. The test itself wasn't wrong.- The forge check passed on the current head (
82c46ebd, 291 s), and the pull merges cleanly into main.
To try it: bun test test/merge-queue-gate-result.test.ts. On the preview, after a gate has run: curl -H "authorization: Bearer $TOKEN" "$URL/api/v1/repos/trevato/forge/queue/gates?limit=5".
🤖 Generated with Claude Code
Closes #6739
Every merge-queue gate that finishes is now stored with the names of the tests that failed in it. The queue log line names them too. GET /api/v1/repos/:o/:n/queue/gates?limit=20 and a new queue_gates MCP tool return them.
Latest push: merges main, which now has #6732's check retry and #6736's README move. The only conflict was the comment at the top of src/checks/queue.ts, and it now describes both changes. No code conflicted. The check-retry and the gate-result tests both pass on the merged tree.
Why "2 failed" probably meant one test. At the end of a run, scripts/test-shards.ts prints every failure a second time, as (fail) name [1ms] [shard 3]. failuresOf didn't strip the [shard N] part, so it counted each failure twice. Both parsers now share one pattern and count each test once.
What changed
- Reading the output:
src/checks/gate_tests.tsreads a gate's output for:
- how many tests passed, added up across shards; - how many different tests failed; - up to 20 failing tests, each with its file and its first 5 error lines.
landing.ts carries this on the red verdict. queue_store.ts keeps it when a gate's verdict is picked up again after a restart.
- Storage: a new migration adds a
gate_resulttable, written bysrc/checks/gate_result.ts. Every finished gate gets a row, whether green, red orcut_off. That covers whole batches, the smaller gates run to find the culprit, and pulls gated alone. A row holds the pulls, the reason, the counts, the failing tests and the duration. Each time a row is written, rows older than 7 days are deleted. - Log line: a red now reads
gated 8 pulls: red (2 failed: "a > b", "c > d"). It names up to 5 tests, then addsand N more. A red that named no test reads as before. - API:
- The answer is { items, next_cursor }, newest first.
- limit is 1–100 and cursor is a gate id.
- A malformed cursor gets a 422. A repository you can't read gets a 404.
- The route names its session, gates and render timing phases. Leaving these out is what failed the check on #6740.
- Flaky flag: a failing test in the answer carries
flaky: truewhen the same test in the same file failed in two red gates that share no pull. Only the last 500 red gates of the kept week are compared.
Not done
- A flaky red is still searched and bisected the same way; the flag doesn't change what the queue does.
- The log line doesn't mark which tests are flaky.
- No web page reads the table yet.
- The issue points to #6643 for a flake list, but #6643 is a pull that raises a migrate test's timeout, and main has no flake list. So the flaky flag reads
gate_resultdirectly instead of an existing list.
Tests
test/merge-queue-gate-result.test.tsis new. It feeds in a fake two-shard output with two failing tests and gets both names once, with their files, error lines and counts. It also covers the 20-test cap, the log wording and the flaky rule.- End to end, it runs
sweepGreenagainst a fake check lane. The log line names both tests. The API and the MCP tool return both, withfailedequal to the number of tests named. It also covers paging, the 404, the 422, the flaky flag and the 7-day pruning. test/mcp.test.tslists every MCP tool by name, soqueue_gateswas added to that list. The test itself wasn't wrong.- After the merge the type check is clean. The merge-queue, gate, MCP and route-timing suites pass: 146 tests across 19 files.
To try it: bun test test/merge-queue-gate-result.test.ts. On the preview, after a gate has run: curl -H "authorization: Bearer $TOKEN" "$URL/api/v1/repos/trevato/forge/queue/gates?limit=5".
🤖 Generated with Claude Code
Closes #6739
Approved. This does what #6739 asks and serves the pipeline-first direction (#5276's line): a red gate now names its failing tests in the queue log, in GET …/queue/gates and in the queue_gates MCP tool. It also fixes the shard tally counting each failure twice.
Security: both doors check access with the existing canRead gate, the same one gates/:id/log already uses (loadRepo on the route, repoArg in the MCP tool), so a private repository answers 404 or not_found and the test covers that. limit is clamped and cursor is checked against ^[0-9A-Za-z]{1,40}$ (422 otherwise). All SQL takes parameters, nothing shells out, and stored rows are capped (20 tests × 5 lines × 300 chars). Rows read back are validated at the boundary (testsOf, pullsOf, gateTestsOf). It is read-only, so there is no write to check, and the error lines it shows come from the same output the gate log already shows to the same readers.
Preview at this head: / answers 200 and the security headers are on the new route. The preview has no public repository with gates, so the route answers 404 for an unreadable repository and the API test covers the populated answer end to end through sweepGreen.
There is no page and no terminal view yet, and the pull says so. Since it adds nothing a person can do on a page, the terminal is not owed one. The doc for it is docs/features/queue-gates.md, a sensible place while the README is at its budget.
One low point is inline. It is not blocking.
Every finished merge-queue gate is now stored with the tests that failed in it. The queue log names them, and
GET /api/v1/repos/:o/:n/queue/gates?limit=20and a newqueue_gatesMCP tool return them.Why "2 failed" probably meant one test. At the end of a run,
scripts/test-shards.tsprints every failure again, as(fail) name [1ms] [shard 3].failuresOfdidn't strip the[shard N]part, so it counted each failure twice. Both parsers now use one pattern and count each test once.What changed
src/checks/gate_tests.tsreads a gate's output for:- how many tests passed, summed across shards; - how many distinct tests failed; - up to 20 failing tests, each with its file and its first 5 error lines.
landing.tscarries this on the red verdict, andqueue_store.tskeeps it when a gate's verdict is picked up again after a restart.gate_resulttable, written bysrc/checks/gate_result.ts. Every gate that finishes gets a row, green, red orcut_off: whole batches, the smaller gates run to find the culprit, and pulls gated alone. Each row holds the pulls, the reason, the counts, the failing tests and the duration. Rows older than 7 days are deleted whenever a new one is written.gated 8 pulls: red (2 failed: "a > b", "c > d"). It names up to 5 tests and addsand N morepast that. A red that named no test reads as before.- It returns
{ items, next_cursor }, newest first. -limitis 1–100 andcursoris a gate id. - A malformed cursor gets a 422, and a repository you can't read gets a 404. - It names itssession,gatesandrendertiming phases. Leaving these out is what failed the check on #6740.flaky: truewhen the same file and test failed in two red gates that share no pull. Only the last 500 red gates of the kept week are compared.Not done
gate_resultitself rather than on an existing list.Tests
test/merge-queue-gate-result.test.tsis new. A fake two-shard output with two failing tests gives both names once, with their files, error lines and counts. It also covers the 20-test cap, the log wording and the flaky rule.sweepGreenagainst a fake check lane. The log line names both tests, and the API and MCP tool return both, withfailedequal to the number named. It also checks paging, the 404, the 422, the flaky flag and the 7-day pruning.test/mcp.test.tslists every MCP tool by name, soqueue_gateswas added to that list. The test wasn't wrong.To try it:
bun test test/merge-queue-gate-result.test.ts. On the preview, once a gate has run:curl -H "authorization: Bearer $TOKEN" "$URL/api/v1/repos/trevato/forge/queue/gates?limit=5".🤖 Generated with Claude Code
Closes #6739