Skip to content

fix(land): keep a PR at the gate's own base merge queued - #378

Merged
moq-bot[bot] merged 2 commits into
mainfrom
fix/land-prepared-head-collect
Nov 2, 2026
Merged

moq-bot[bot] merged 2 commits into
mainfrom
fix/land-prepared-head-collect

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

moq-dev/quest#342 was parked right after the gate pushed its own base merge, then handed back by the fixer as "head moved". Both steps were gate bugs; this fixes both.

Root cause (from the host's state, journal, and batch log, 2026-11-01)

  1. The batch with quest: plan the 2026-11-01 follow-ups #373 and feat(gate): admit a lone command on a host loaded by other work #342 passed its check. quest: plan the 2026-11-01 follow-ups #373 landed as 3b4e77c2, and the gate pushed aa87d974 to feat(gate): admit a lone command on a host loaded by other work #342's branch at 22:51:35Z. aa87d974 is quest land: merge tested base, with parents pin 72750ef7 and base 3b4e77c2, recorded in head-342-aa87d97….json.
  2. The squash API refused it: GitHub refused /repos/moq-dev/quest/pulls/342: Pull Request has merge conflicts (end of batch-1793572781096-323.log). The daemon then logged parked 342. In that build, only merge_conflict parks; head_moved ejects. So GitHub already showed aa87d974 as the head but called it conflicting, using mergeability left over from the pin, which does conflict with 3b4e77c2 without the quest driver. feat(gate): admit a lone command on a host loaded by other work #342 now reports mergeable: true at aa87d974, so the report was stale.
  3. The fixer job held the batch plan's entry at the pin, 72750ef7. On collection, the check current !== entry.head && !(published && current === head) treated the gate's own aa87d974 as a writer's commit and handed feat(gate): admit a lone command on a host loaded by other work #342 back.

Changes

  • mergePinned (src/cli/land-serve.ts): if GitHub calls the gate's freshly pushed merge conflicting (mergeable: false, dirty, or a "merge conflicts" refusal), the gate treats it as unsettled mergeability and retries. That merge holds the base it was tested on, so it cannot conflict with it. Only a base that moved since can make it conflict; then the retries run out, the round throws, and the next round rebuilds on that base. A conflict on a head the gate did not just push still parks as before. The settle backoff grows from 3.5 s to 15.5 s (5 retries), since feat(gate): admit a lone command on a host loaded by other work #342's refusal came 5 s after the push.
  • collectJob (src/cli/land-fixer.ts): recognizes the gate's privately recorded merge for this queue entry, using isPrepared (now exported), the same check resolvePreparedQueue uses. The record must match the same pin, the same enabledAt, parents pin + base, the tested tree, and the bot author. Author text alone is never trusted. When the PR stands at that merge:
    • requeue: re-pinned at the gate's merge, keeping the pick's place through the attempt record.
    • fixed: the fix descends from the pin, so it replaces the gate's merge with a lease on that merge, the way prepareHead already replaces its own earlier merge. Building on that merge would revert the newer base it merged. If GitHub refuses that push, the handback carries no quest land apply resolution: that resolution is built from the pin, and apply refuses a PR whose head moved.
    • handback: hands back with the fixer's own diagnosis, not "head moved".
    • A forged copy of the merge (same tree, parents, author, and message, different timestamp) or a writer's commit on it still hands back as a moved head.
  • push (src/cli/land-head.ts): asks for workflow write when the pushed head changes workflows relative to either the head it builds on or the head it replaces.
  • README and the Local mergeability quest's Plan describe the new behavior. The quest keeps the remaining case: a head the gate did not merge that round, which GitHub flags anyway.

Choices for review

  • A conflict report that persists on the gate's fresh merge throws for a rebuild instead of parking. Parking would blame the PR and spend its pick's one fixer attempt on something it didn't cause. The cost is one extra batch check. On an unmoved base, the next round merges the recorded head without a push, so a persistent conflict report there still parks (covered in the updated "a refused second merge" test).
  • requeue at the gate's merge re-pins it there, as the brief asked, instead of leaving the pin on approved and relying on the private record. That records on GitHub which head will land, and it no longer depends on the record surviving.
  • fixed replaces the gate's merge (force-with-lease on it) instead of rebuilding the fix on top of it, for the revert reason above. The next batch re-merges the base anyway.

Tests

New regression tests in test/land.test.ts, each reproducing a step of the #342 sequence:

  • A 405 "merge conflicts" refusal while GitHub already shows the gate's merge, with mergeable: false or dirty, is retried and lands.
  • A conflict report that persists throws "mergeability did not settle". The PR stays queued with no comment, and the next round lands the recorded merge.
  • Collection at the gate's recorded merge after it was pushed while the PR was parked: requeue (pinned there), fixed (replaces it and is pinned), and handback (keeps the fixer's diagnosis).
  • A forged copy, or a writer's commit, at that merge still hands back as a moved head.

The 6 behavior tests fail on origin/main. The 2 safety tests pass both before and after. The "a refused second merge (merge_conflict)" and "classifies a refused merge" tests were updated for the new retry behavior.

Local checks: just check passes, and test/land.test.ts, test/landed.test.ts, and test/land-fixer-diagnostics.test.ts pass, 187/187, with CLAUDE_CONFIG_DIR unset.

Follow-ups

Review

  • Reviewer: Codex (quest review), gpt-6.1-sol, medium effort.
  • Reviewed head: 61933a9204e913ca4c0e430e24c989a2f20908b6 against origin/main (97ed0ff8).
  • Round 1: no findings.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

#342 was parked for the fixer right after the gate pushed its
own merge of the tested base: GitHub already showed that merge as the head,
but still called it conflicting from the pin's stale mergeability, and
mergePinned returned merge_conflict at once. That merge holds the base it was
checked on, so it cannot conflict with it. A conflict GitHub reports on the
gate's freshly pushed merge is now retried as unsettled mergeability; if it
persists, the round throws and the next one rebuilds the queue on the base as
it then is. The settle retries grow from 3.5 s to 15.5 s of backoff.

The fixer then handed #342 back as "head moved", since collection took the
gate's merge for a writer's commit. Collection now recognizes the gate's
privately recorded merge for that queue entry, as resolvePreparedQueue does:
a requeue pins the PR at it, a handback keeps the fixer's diagnosis, and a
fix, built on the pin, replaces it with a lease on it, since building on it
would revert the newer base it merged. A forged copy or a writer's commit
still hands back as a moved head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d398dc30-42aa-40e3-9767-0d6b2d266be0
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated
kixelated enabled auto-merge (squash) November 1, 2026 23:31
@moq-bot
moq-bot Bot merged commit 1c84b14 into main Nov 2, 2026
@moq-bot
moq-bot Bot deleted the fix/land-prepared-head-collect branch November 2, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant