Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,22 +470,28 @@ hold a PR like any `quest:*` label; passing their token to `quest iterate`
swaps them for `quest:active`. Their `quest:merge` holds nothing and is
ignored.

`quest merge` takes no label: the head-pinned auto-merge it enables is the
queue signal, so a queued PR shows no hold. It refuses while any session holds
the PR, so release your own hold first. `--land` needs the full 40-character
`quest merge` takes no label: the head-pinned auto-merge or merge-queue entry
it creates is the queue signal, so a queued PR shows no hold. It refuses while
any session holds the PR, so release your own hold first. `--land` needs the full 40-character
head SHA and refuses once the head moves.
It enables auto-merge bound to that SHA, which enters GitHub's merge queue
when the base has one, or merges at once when nothing is left to wait for.
`--squash` or `--rebase` picks the method, and it refuses one the repository
disallows; otherwise it uses the only method the repository allows, or else a
merge commit, and refuses when merge commits are off and more than one method
remains. A merge queue applies its own. On a base that requires the `quest land` check, it
It enables auto-merge bound to that SHA, or merges at once when nothing is
left to wait for. `--squash` or `--rebase` picks the method, and it refuses one
the repository disallows; otherwise it uses the only method the repository
allows, or else a merge commit, and refuses when merge commits are off and more
than one method remains. On a base that requires the `quest land` check, it
queues the PR for [`quest land`](#land-a-batch-with-quest-land) instead.
On an ungated base with auto-merge disabled, it refuses. Follow the repository’s own landing
On a base with GitHub's merge queue, which applies its own method, it enqueues
that head, or arms auto-merge bound to it while required checks may still be
pending. The queue builds each PR on the current base, so a head behind it is
enqueued as is. When GitHub refuses to enqueue the head, such as after the
base moved into a conflict, the command fails: fix the PR, for example with
`/quest-iterate`, and land it again.
On a base with neither the `quest land` check nor a merge queue, it refuses
while repository auto-merge is disabled. Follow the repository’s own landing
instructions in CONTRIBUTING.md or AGENTS.md, such as its label queue; never
retry with `--direct` unless the user asks. `--direct` permits the usual landing
behavior despite that repository setting.
On an ungated base, it compares the live base against that head and refuses a
On such a base, it also compares the live base against that head and refuses a
head that is behind or diverged: merge the base, rerun the repository's checks,
and land the new head. This is a preflight check; GitHub can still move the base
between that comparison and its head-pinned merge mutation. A PR that leaves
Expand Down
5 changes: 3 additions & 2 deletions assets/skills/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ A finding means the PR leaves its quest inaccurate: recommend `/quest-iterate`.
Queue the head with `quest merge <pr> --land <full 40-character head SHA>`.
It uses the only merge method the repository allows, or else a merge commit; add `--squash` or `--rebase` when the repository's root instructions name that method for the PR's base.
On a base that requires the `quest land` check, it queues the PR for `quest land`, whose chain merges and tests against the current base.
On an ungated base with auto-merge disabled, it refuses. Follow the repository’s own landing instructions (for example, its label queue); never retry with `--direct` unless the user explicitly asks.
On an ungated base, it refuses a head that does not contain the current base: recommend `/quest-iterate` to merge the base.
On a base with GitHub's merge queue, it enqueues the head; if GitHub refuses, such as after the base moved into a conflict, it fails: recommend `/quest-iterate`.
On an ungated base without a merge queue, it refuses while auto-merge is disabled. Follow the repository’s own landing instructions (for example, its label queue); never retry with `--direct` unless the user explicitly asks.
There, it also refuses a head that does not contain the current base: recommend `/quest-iterate` to merge the base.

Report once it is queued, and do not wait for it to merge.
Plan the PR's suggested follow-ups as soon as it is queued, as if it lands, rather than waiting for the merge, unless a coordinator such as `/quest-complete` called this skill and batches them.
Expand Down
5 changes: 3 additions & 2 deletions quest/m0/land-fixer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ Decision prompts, selected options marked ✅:
this epic keeps only the fixer's own quality work. Moved to the watcher:
the attempt budget (now the stop rules,
[moq-dev/quest#384](https://github.com/moq-dev/quest/pull/384)),
local mergeability and CI-failure parking (now
[Bad picks](/quest/m0/queue-watcher/bad-picks.md)), and flaky tests (now
local mergeability and CI-failure parking (then
[Bad picks](/quest/m0/queue-watcher/bad-picks.md), dropped 2026-11-02: a
pick GitHub refuses fails `quest merge` instead), and flaky tests (now
[Flaky failures](/quest/m0/queue-watcher/flaky.md), without the rerun).
Deleted: the batch quest check, pre-checks, and throughput tuning, which
only served batches (the fixer cap and reasoning effort moved to
Expand Down
5 changes: 3 additions & 2 deletions quest/m0/land-fixer/pr-signal.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ non-required check failed on the head it is about to pick, without refusing.
The fixer gets a summary of the PR's checks (check name, conclusion, run URL,
and a trimmed log of each failure) in its job input, next to the queue's
failure log. A failing non-required check never stops a PR on its own; a
failing required check keeps GitHub from enqueuing it, which
[Bad picks](/quest/m0/queue-watcher/bad-picks.md) handles.
failing required check keeps GitHub from enqueuing it: `/quest-merge`
refuses such a head, `quest merge` fails when GitHub refuses it, and either
way the PR needs `/quest-iterate`.

Retargeted 2026-11-01 to the [queue watcher](/quest/m0/queue-watcher/README.md),
which replaces the batched host gate.
Expand Down
30 changes: 25 additions & 5 deletions quest/m0/queue-watcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,30 @@ Decision prompts, selected options marked ✅:
- ✅ The fixer re-enqueues them unchanged, and a test seen flaking twice is
disabled in a separate PR with a follow-up quest; the rerun before blame
(#375) is dropped. See [Flaky failures](/quest/m0/queue-watcher/flaky.md).
- Picks GitHub refuses to enqueue:
- ✅ The watcher fixes them, then enqueues; see
[Bad picks](/quest/m0/queue-watcher/bad-picks.md)
- Pick record (decided 2026-11-02, replacing "Picks GitHub refuses to
enqueue: ✅ The watcher fixes them"):
- ✅ GitHub's records only. Drop the comment and `pick.ts`. The watcher
reads queue events and auto-merge state. A pick GitHub refuses fails the
command, and /quest-merge recommends /quest-iterate as it does today.
`bad-picks.md` shrinks to enqueueing PRs with auto-merge armed that
GitHub never queued
([community#209348](https://github.com/orgs/community/discussions/209348)).
- Comment only on refusal
- Keep the comment on every pick

Reasons: for queued picks, GitHub already keeps a durable,
writer-authenticated record. `AddedToMergeQueueEvent` and
`RemovedFromMergeQueueEvent` give the enqueuer (who must have write
access), the head before removal, and the reason, and `autoMergeRequest`
gives `enabledBy`/`enabledAt` for picks waiting on checks. Auto-merge can't
reliably be armed on a conflicting PR, and on merge-queue bases enabling it
returns HTTP 422 unless every requirement is met
([community#190610](https://github.com/orgs/community/discussions/190610),
[community#50327](https://github.com/orgs/community/discussions/50327)).
/quest-merge already refuses heads with failing checks or conflicts, so a
refusal only happens if the base moves between that check and the enqueue.
See [Queue removals](/quest/m0/queue-watcher/removals.md) and
[Bad picks](/quest/m0/queue-watcher/bad-picks.md).
- Milestone:
- ✅ m0, at the top

Expand Down Expand Up @@ -109,10 +130,9 @@ Shared shape, for the children to follow:

## Required

- [Picks enter the merge queue](/quest/m0/queue-watcher/pick.md) - `quest merge --land` enqueues the pinned head into GitHub's merge queue and records the pick where the watcher reads it
- [Queue removals](/quest/m0/queue-watcher/removals.md) - the watcher sees each PR the queue drops, why, and the failing build's log
- [Fix and re-enqueue](/quest/m0/queue-watcher/fix.md) - the fixer runs on each dropped PR in the background, and the watcher re-enqueues it at the back
- [Bad picks](/quest/m0/queue-watcher/bad-picks.md) - a pick GitHub refuses to enqueue is merged locally with the quest driver or fixed, then pushed and enqueued
- [Bad picks](/quest/m0/queue-watcher/bad-picks.md) - a pick whose auto-merge is armed on a ready PR GitHub never queued is enqueued at the head it was armed on
- [Flaky failures](/quest/m0/queue-watcher/flaky.md) - a flaky failure is re-enqueued unchanged, and a test seen flaking twice is disabled in a separate PR with a follow-up quest
- [Watcher on the host](/quest/m0/queue-watcher/host.md) - kixelated/nixos runs the watcher for moq-dev/moq beside its fixer container
- [The other repositories follow](/quest/m0/queue-watcher/follow.md) - condition: moq's queue is proven; then moq-dev/quest switches and the others plan theirs
Expand Down
75 changes: 35 additions & 40 deletions quest/m0/queue-watcher/bad-picks.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
# [M] The watcher fixes a pick GitHub refuses to enqueue
# [S] The watcher enqueues an armed pick GitHub never queued

## Goal

A picked PR that GitHub refuses to enqueue, because it conflicts with the
base or otherwise can't merge, is fixed by the watcher instead of handed back.
The watcher merges the base into it locally with the quest merge driver; a
clean merge is pushed and enqueued. When the local merge still conflicts, or
the PR's own required checks fail, the fixer resolves it with the conflicting
paths or the failing checks' logs, and the watcher pushes and enqueues the
result. The PR reaches a person only on a handback. The watcher never trusts
GitHub's mergeability flag alone.
A PR on a merge-queue base whose auto-merge a writer armed, as
`quest merge --land` does while required checks are pending, and that is now
ready (its required checks passed and it doesn't conflict) but still not in
the queue, is enqueued by the watcher at the head auto-merge was armed on.
Since about 2026-09-10 GitHub often leaves such PRs out of the queue
([community#209348](https://github.com/orgs/community/discussions/209348)),
while `enqueuePullRequest` works. A head pushed since by anyone but the fixer
is not enqueued.

## Plan

Decided 2026-11-01 (maintainer, /quest-plan; see the
[epic](/quest/m0/queue-watcher/README.md)):

- Picks GitHub refuses to enqueue:
- ✅ The watcher fixes them, including conflicts resolved with the quest
merge driver, then pushes and enqueues
- Hand them back

This quest was the host gate's local mergeability quest, decided 2026-10-31
(user, /quest-plan of the follow-ups from a moq-dev/moq `/quest-complete`
run): GitHub reported conflicts on
[moq-dev/moq#7407](https://github.com/moq-dev/moq/pull/7407), which merges
cleanly, and on [moq-dev/moq#7382](https://github.com/moq-dev/moq/pull/7382),
whose only conflict was in quest link lists, which the driver resolves. It
also absorbs the host gate's plan to park a PR whose own required CI fails for
the fixer with the failing check's log (decided 2026-11-01, settling
[moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355)'s decision 8),
since on a merge-queue base such a PR is a pick GitHub won't enqueue.

- The watcher learns of refused picks from the marker
[Picks enter the merge queue](/quest/m0/queue-watcher/pick.md) records, and
from picks whose auto-merge stays armed while the PR is conflicting or its
required checks failed.
- Merge locally with `mergeBase` (`src/cli/land.ts`), which already uses the
driver; the host gate's `prepareHead` (`src/cli/land-head.ts`) shows the
push. A clean driver merge needs no review: it is a conflict-free base merge.
- Fetch failing required checks' logs through the App's `checks: read` and
`actions: read`, trimmed like a queue failure's.
- The same stop rules apply (`stopRule` in `src/cli/land-fixer.ts`).
Decided 2026-11-02 (maintainer, /quest-plan; see the
[epic](/quest/m0/queue-watcher/README.md)), narrowing this quest from fixing
every pick GitHub refuses: GitHub's records are the only pick record, and a
pick GitHub refuses to enqueue fails `quest merge`, which says to fix the PR
with `/quest-iterate` and land it again. The plans this quest held for
refused picks are dropped with it: merging the base locally with the quest
driver (the host gate's local mergeability, from
[moq-dev/moq#7407](https://github.com/moq-dev/moq/pull/7407) and
[moq-dev/moq#7382](https://github.com/moq-dev/moq/pull/7382)), and parking a
PR whose own required checks fail for the fixer
([moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355)'s
decision 8).

- Find the candidates in the pass
[Queue removals](/quest/m0/queue-watcher/removals.md) runs: open PRs with an
`autoMergeRequest`, no `mergeQueueEntry`, and a ready `mergeStateStatus`
(`CLEAN`, `HAS_HOOKS`, or `UNSTABLE`).
- Enqueue with `enqueuePullRequest` and `expectedHeadOid` set to the head
`enabledHead` (`src/cli/status.ts`) says auto-merge was armed on, only when
that is the current head. GitHub keeps auto-merge across a writer's push,
but that push ends the pick; a head `enabledHead` can't tell yet waits for
the next pass. An "already queued" refusal means GitHub moved it first.
- An armed pick whose own required checks fail, or that turns conflicting,
is left alone: it needs `/quest-iterate` and a new pick, and
`quest status` shows its checks.

## Required

- [Fix and re-enqueue](/quest/m0/queue-watcher/fix.md) - the fixer path and re-enqueue this reuses
- [Picks enter the merge queue](/quest/m0/queue-watcher/pick.md) - the pick marker a refused pick leaves
- [Queue removals](/quest/m0/queue-watcher/removals.md) - the pass that reads each PR's auto-merge and queue state
44 changes: 0 additions & 44 deletions quest/m0/queue-watcher/pick.md

This file was deleted.

36 changes: 22 additions & 14 deletions quest/m0/queue-watcher/removals.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,28 @@ Decided 2026-11-01 (maintainer, /quest-plan; see the
[epic](/quest/m0/queue-watcher/README.md)).

- Poll, don't receive webhooks: the host has no public endpoint. Reconcile
from the picks, not from queue snapshots alone: a PR enqueued and dropped
between two polls, or while the watcher was down, never shows in a
snapshot. Each pass reads every open PR holding an active pick (the marker
[Picks enter the merge queue](/quest/m0/queue-watcher/pick.md) records) with
its `mergeQueueEntry` and its `RemovedFromMergeQueueEvent`s newer than the
last one handled, with `reason`, `actor`, and `enqueuer`. `quest status`
already reads that event (`src/cli/status.ts`). The base's
`mergeQueue { entries }` can still serve as a cheap change signal. The App's
installation token has its own rate limit, separate from the maintainers'
sessions.
from GitHub's records of each pick, not from queue snapshots alone: a PR
enqueued and dropped between two polls, or while the watcher was down,
never shows in a snapshot. Each pass reads the base's open PRs, many per
GraphQL query, with their `mergeQueueEntry`, their
`autoMergeRequest { enabledBy enabledAt }`, and their
`AddedToMergeQueueEvent`s and `RemovedFromMergeQueueEvent`s newer than the
last one handled (`timelineItems(since:)`): `enqueuer`, `actor`, `reason`,
and the removal's `beforeCommit`. `quest status` already reads the removal
event (`src/cli/status.ts`). The base's `mergeQueue { entries }` can still
serve as a cheap change signal. The App's installation token has its own
rate limit, separate from the maintainers' sessions.
- A pick is GitHub's own record, with no marker of quest's
(decided 2026-11-02; see the [epic](/quest/m0/queue-watcher/README.md)):
the PR was enqueued, or had auto-merge armed while its required checks were
pending, as `quest merge --land` does on a merge-queue base. GitHub lets
only a writer enqueue a PR or enable auto-merge, so the `enqueuer` or
`enabledBy` is the trust signal; nothing reads comments or checks a
commenter's permission. The removal's `beforeCommit` is the head the queue
dropped. A pick covers that head and the heads the fixer pushes from it,
which the watcher's App then enqueues itself, and stays active until a
handback, a push by anyone else, or a person's removal; a writer enqueuing
the PR again starts a new one.
- For a failed build, find the PR's `merge_group` workflow runs (event
`merge_group`, head branch `gh-readonly-queue/<base>/pr-<n>-<sha>`) and
fetch the failing jobs' logs through the App's `actions: read`, trimmed the
Expand All @@ -39,7 +51,3 @@ Decided 2026-11-01 (maintainer, /quest-plan; see the
- Start it as `quest land watch` beside `quest land serve`, sharing its App
credential options, state directory, and comment marker and
deduplication.

## Required

- [Picks enter the merge queue](/quest/m0/queue-watcher/pick.md) - the durable pick marker the watcher reconciles from
Loading
Loading