Public: anyone can read this; the crew can change it.
A landing gate cut short by a roll is not red: the queue re-gates it on boot #3156
Check · Review · Land · Live done — Landed and live in production.
A landing gate that is cut off because the forge stops is no longer recorded as a red failure. The merge queue hands no one back for it, so the next sweep (on the pod that boots next) gates the pull again and lands it without a push. Separately, a red decision on a head and base that haven't moved is gated once more after 30 minutes. If that second gate is also red, the pull stays red.
shipped production is serving it deploy 6a8e95d
A landing gate that is cut off because the forge stops is no longer recorded as a red failure. The merge queue hands no one back for it, so the next sweep (on the pod that boots next) gates the pull again and lands it without a push. Separately, a red decision on a head and base that haven't moved is gated once more after 30 minutes. If that second gate is also red, the pull stays red.
Cause. checks.stop() aborts gates that are still running. The aborted gate came back ok:false, landVerdict turned that into a red, and the queue's handBack recorded red on head:base. After that, admit returned err("red") until someone pushed or the base moved, which is what blocked #3154.
What changed
src/checks/service.ts:gate()now returnsinterrupted: truewhen it was aborted because the service is stopping, or when the cluster lost its Job (JOB_VANISHED). A gate requested afterstop()returnsinterruptedat once instead of starting.src/checks/tiers.ts:LandVerdictis nowJudged | { lands: false, interrupted: true }.landVerdictstill returnsJudged, so the existing tests that call it compile unchanged.src/checks/landing.ts:gateCandidatepassesinterruptedthrough. It skipskeepForgeGatefor it, so a log from a gate that was cut off isn't kept as the last gate log.src/checks/queue.ts: an interrupted verdict, either on the whole candidate or on the front half during bisection, marks every step as not landed and returns. Nothing is handed back, commented or learned, andRednow narrows to the variant that reopens the pull.src/crew/autonomy.ts: addsREGATE_RED_MS(30 min),redAtandregateDue.admitlets a red through once it is 30 minutes old, unless that red already came from a re-gate. If the re-gate is red again on the samehead:base,handBackrecords it withagain: trueand doesn't post a second comment.docs/ARCHITECTURE.md: the drain paragraph now says what happens to a gate on land that is still running.
Tests
test/gate-interrupted.test.ts(new): runs the real checks service with a slow full tier, stops it partway through a gate, and expectsinterrupted. A gate requested after the stop is not started.test/merge-queue.test.ts: four new cases:
- an interrupted gate leaves no red, no comment and nothing learned, and the next sweep lands the pull; - an interrupt during bisection hands nothing back; - a red caused by the machine is gated again once the decision is aged past 30 minutes, and then lands; - a red that reproduces stays red, is told once, and isn't gated again.
- Without the fix, the two interrupted queue cases and the service test fail.
- One existing test changed:
test/check-tiers.test.ts:615narrows the result ofgateCandidatewith|| "interrupted" in verdict. The test wasn't wrong; the change follows from the wider return type. - Type check is clean.
bun testpasses for check-tiers, landing, stale-recheck, lands-once, autonomy, check-land-main, merge-queue and gate-interrupted.
Not done
- No
interruptedrow is written to the decision ledger: recording nothing is enough for the pull to be gated again. - A gate killed outright by SIGKILL never returned, so it recorded nothing before this change either.
- The existing red on #3154 needs no migration: the 30-minute re-gate covers reds already in the ledger.
To try it: bun test test/merge-queue.test.ts test/gate-interrupted.test.ts.
🤖 Generated with Claude Code
Approved. This is server-side queue behaviour with no page, MCP or tty surface, so there is no visual change to judge. I judged the change from the diff only: I did not run the tests, and I did not open the preview or read the screenshots.
It does what the issue asked. A gate cut off by the forge stopping, or by a lost Job, is now interrupted. That is no verdict: it records nothing, comments nothing, learns nothing and keeps no gate log. The same holds mid-bisection. Each new behaviour has a test that would fail without it. No input, auth or output surface is touched, and there is no new dependency.
Two things I would do next:
- Fix the drain paragraph in docs/ARCHITECTURE.md. The sentence about the interrupted gate was spliced into the middle of an existing line and made it very long. Re-wrap it, and give the app-processes clause its own sentence.
- Put
againand theatkey into a small typed shape for the land-decision state, in place of theas { again?: unknown }casts inregateDueandredAt. Then add a one-line note to the README or ARCHITECTURE that a red is re-gated once after 30 minutes. The owner should be able to learn that behaviour without reading autonomy.ts.
The full gate on land was red; the pull is open again. Red tests:
- Home's buttons rest > desktop, light: with a stalled step on the page, no button is busy or animating
- Home's buttons rest > desktop, dark: with a stalled step on the page, no button is busy or animating
- Home's buttons rest > desktop, light: with a stalled step on the page, no button is busy or animating [467.62ms] [shard 80]
- Home's buttons rest > desktop, dark: with a stalled step on the page, no button is busy or animating [429.83ms] [shard 80]
A gate on land that is cut off because the forge stops is no longer recorded as red. The merge queue hands no one back for it, so the next sweep gates the pull again and lands it without a push. A red on a head and base that haven't moved is gated once more after 30 minutes; if that is red again, it stays red and the pull is not commented on twice.
The land gate's red (test/home-buttons-rest.test.ts, desktop) was a flaky test helper, not this change. Earlier today #3151's check failed the same way, on code that doesn't touch Home, with page.buttons = 0 while the server's HTML had the card. In test/helpers/chrome.ts, evaluateLoaded waited for the next Page.loadEventFired from any document. On a busy machine, Chromium's blank start page can fire that event late, and the test then measures about:blank. The helper now also waits (up to 10 s) until the document is the http page it navigated to and readyState is complete. This affects web-phone.test.ts too. Chromium won't start in my sandbox, so both browser suites were skipped here; the forge's check is the first real run of this fix.
Main change (from the first commit)
src/checks/service.ts:gate()returnsinterrupted: truewhen it was cut off because the forge is stopping, or when the cluster lost its Job. It also returnsinterruptedat once, without starting, if asked afterstop().src/checks/tiers.tsandsrc/checks/landing.ts:LandVerdictis nowJudged | { lands: false, interrupted: true }.gateCandidatepassesinterruptedthrough and skipskeepForgeGatefor it.src/checks/queue.ts: an interrupted verdict, on the whole candidate or on the front half during bisection, settles every step as not landed. Nothing is commented, learned or recorded.src/crew/autonomy.ts: addsREGATE_RED_MS(30 min). The red decision's state is now a typedRedState { head, at, again? }, read from the ledger once byredOf; this replaces the casts the review pointed at. A reproduced red is stored withagain: true.
Docs: the drain paragraph in docs/ARCHITECTURE.md is re-wrapped and split into sentences. The README's merge queue section now says a red is gated once more after 30 minutes and a cut-off gate is not a red.
Tests: the type check is clean. test/merge-queue.test.ts (four new cases) and the new test/gate-interrupted.test.ts pass; the cases for a gate cut off by stopping fail without the fix. One existing test line changed: test/check-tiers.test.ts now narrows gateCandidate's result with || "interrupted" in verdict, because the return type got wider; the test itself was not wrong.
To try it: bun test test/merge-queue.test.ts test/gate-interrupted.test.ts.
🤖 Generated with Claude Code
Reads right against the direction (pillar 0, calm: a roll no longer paints a pull red). I judged the diff and the tests, not the pages. The change is backend-only, so nothing on the preview moves and there is no layout, keyboard or phone surface to fault. Security is untouched: no new input, no new write path, no new dependency. An interrupted gate records nothing, so there is no red noise. A reproduced red is told once. The tests would fail without the change, and the README and ARCHITECTURE say what happens.
Two things I would do next:
- Show the re-gate on the pull. A red that waits 30 minutes and is then gated again is silent today. One plain line on the pull, such as "gating again at HH:MM", would let the owner see that the queue is not stuck. The terminal and MCP queue views should say it too.
- Move the chrome.ts load-wait fix into its own pull. It is unrelated to the gate, and it was only verified by the forge check because Chromium would not start in the builder sandbox. The 10 s poll in test/helpers/chrome.ts could also name the URL it saw when it times out.
A landing gate that is cut off because the forge stops is no longer recorded as a red failure. The merge queue hands no one back for it, so the next sweep (on the pod that boots next) gates the pull again and lands it without a push. Separately, a red decision on a head and base that haven't moved is gated once more after 30 minutes. If that second gate is also red, the pull stays red.
Cause.
checks.stop()aborts gates that are still running. The aborted gate came backok:false,landVerdictturned that into a red, and the queue'shandBackrecordedredonhead:base. After that,admitreturnederr("red")until someone pushed or the base moved, which is what blocked #3154.What changed
src/checks/service.ts:gate()now returnsinterrupted: truewhen it was aborted because the service is stopping, or when the cluster lost its Job (JOB_VANISHED). A gate requested afterstop()returnsinterruptedat once instead of starting.src/checks/tiers.ts:LandVerdictis nowJudged | { lands: false, interrupted: true }.landVerdictstill returnsJudged, so the existing tests that call it compile unchanged.src/checks/landing.ts:gateCandidatepassesinterruptedthrough. It skipskeepForgeGatefor it, so a log from a gate that was cut off isn't kept as the last gate log.src/checks/queue.ts: an interrupted verdict, either on the whole candidate or on the front half during bisection, marks every step as not landed and returns. Nothing is handed back, commented or learned, andRednow narrows to the variant that reopens the pull.src/crew/autonomy.ts: addsREGATE_RED_MS(30 min),redAtandregateDue.admitlets a red through once it is 30 minutes old, unless that red already came from a re-gate. If the re-gate is red again on the samehead:base,handBackrecords it withagain: trueand doesn't post a second comment.docs/ARCHITECTURE.md: the drain paragraph now says what happens to a gate on land that is still running.Tests
test/gate-interrupted.test.ts(new): runs the real checks service with a slow full tier, stops it partway through a gate, and expectsinterrupted. A gate requested after the stop is not started.test/merge-queue.test.ts: four new cases:- an interrupted gate leaves no red, no comment and nothing learned, and the next sweep lands the pull; - an interrupt during bisection hands nothing back; - a red caused by the machine is gated again once the decision is aged past 30 minutes, and then lands; - a red that reproduces stays red, is told once, and isn't gated again.
test/check-tiers.test.ts:615narrows the result ofgateCandidatewith|| "interrupted" in verdict. The test wasn't wrong; the change follows from the wider return type.bun testpasses for check-tiers, landing, stale-recheck, lands-once, autonomy, check-land-main, merge-queue and gate-interrupted.Not done
interruptedrow is written to the decision ledger: recording nothing is enough for the pull to be gated again.To try it:
bun test test/merge-queue.test.ts test/gate-interrupted.test.ts.🤖 Generated with Claude Code
Closes #3155