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

Merged opened this in answer to

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

Merged

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

opened this

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 returns interrupted: true when it was aborted because the service is stopping, or when the cluster lost its Job (JOB_VANISHED). A gate requested after stop() returns interrupted at once instead of starting.
  • src/checks/tiers.ts: LandVerdict is now Judged | { lands: false, interrupted: true }. landVerdict still returns Judged, so the existing tests that call it compile unchanged.
  • src/checks/landing.ts: gateCandidate passes interrupted through. It skips keepForgeGate for 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, and Red now narrows to the variant that reopens the pull.
  • src/crew/autonomy.ts: adds REGATE_RED_MS (30 min), redAt and regateDue. admit lets 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 same head:base, handBack records it with again: true and 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 expects interrupted. 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:615 narrows the result of gateCandidate with || "interrupted" in verdict. The test wasn't wrong; the change follows from the wider return type.
  • Type check is clean. bun test passes for check-tiers, landing, stale-recheck, lands-once, autonomy, check-land-main, merge-queue and gate-interrupted.

Not done

  • No interrupted row 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

Closes #3155

commented

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 returns interrupted: true when it was aborted because the service is stopping, or when the cluster lost its Job (JOB_VANISHED). A gate requested after stop() returns interrupted at once instead of starting.
  • src/checks/tiers.ts: LandVerdict is now Judged | { lands: false, interrupted: true }. landVerdict still returns Judged, so the existing tests that call it compile unchanged.
  • src/checks/landing.ts: gateCandidate passes interrupted through. It skips keepForgeGate for 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, and Red now narrows to the variant that reopens the pull.
  • src/crew/autonomy.ts: adds REGATE_RED_MS (30 min), redAt and regateDue. admit lets 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 same head:base, handBack records it with again: true and 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 expects interrupted. 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:615 narrows the result of gateCandidate with || "interrupted" in verdict. The test wasn't wrong; the change follows from the wider return type.
  • Type check is clean. bun test passes for check-tiers, landing, stale-recheck, lands-once, autonomy, check-land-main, merge-queue and gate-interrupted.

Not done

  • No interrupted row 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 — before the latest push

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:

  1. 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.
  2. Put again and the at key into a small typed shape for the land-decision state, in place of the as { again?: unknown } casts in regateDue and redAt. 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.
commented

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]
commented

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() returns interrupted: true when it was cut off because the forge is stopping, or when the cluster lost its Job. It also returns interrupted at once, without starting, if asked after stop().
  • src/checks/tiers.ts and src/checks/landing.ts: LandVerdict is now Judged | { lands: false, interrupted: true }. gateCandidate passes interrupted through and skips keepForgeGate for 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: adds REGATE_RED_MS (30 min). The red decision's state is now a typed RedState { head, at, again? }, read from the ledger once by redOf; this replaces the casts the review pointed at. A reproduced red is stored with again: 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

Approved

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:

  1. 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.
  2. 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.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs