# Stack Merge Queue Target

## Goal

Make every AGS path that decides whether a pull request uses a Merge Queue use
the same target branch. For a normal pull request this is `pull_request.base_ref`.
For every member of a Stack it is the Stack's final `base_ref`.

## Problem

A Stack has two relevant branches for an upper pull request:

- its direct parent branch, used to show that individual review diff;
- the Stack's final base branch, where the selected Stack prefix ultimately
  merges or queues.

AGS async Stack submission and direct enqueue already used the final base, but
GraphQL `isMergeQueueEnabled` and the no-entry Queue lookup could use the direct
base. A three-layer Stack with a Queue only on `main` could therefore submit to
the Queue while gh-web reported that the Queue was disabled and offered a merge
method the Queue would ignore.

GitHub's documented asynchronous Stack merge is atomic: the selected prefix is
merged or queued into the Stack base as one unit. AGS must make the Queue
configuration query answer for that same target.

## Design

`service.MergeQueueTargetForPullRequest` is the single branch resolver. One
compact membership query returns the final Stack base plus Stack ID; a normal
pull request returns its direct base and a zero Stack ID. Read paths do not
hydrate every Stack member merely to answer Queue configuration.

Queue mutation uses a repository-scoped Stack topology lock. Stack create,
append, and unstack operations acquire the same lock as direct and async Queue
submission. Submission then locks and reloads the target pull request, resolves
the target, hydrates the selected Stack prefix, checks Queue configuration, and
creates Queue entries inside that one transaction. It never carries a target
base across the transaction boundary or re-reads membership after selecting a
different snapshot.

After submission, an active direct Stack merge request protects the full
bottom-to-target prefix even though the request row belongs only to the target
pull request. Unstack may not remove a prefix member that the worker will merge.
The async request's `expires_at` is the public result-retention deadline, not an
execution lease. Pending, merging, and cascading requests continue to protect
topology until they reach a terminal state; recovery scans them even after an
old retention deadline and renews the 24-hour result window before resuming.
Duplicate submission renews only `expires_at`; `updated_at` remains the
execution-progress and recovery-attempt clock so observation cannot postpone
recovery of abandoned work.

The following callers use the resolver:

- async merge submission, including its Queue batch creation;
- async worker revalidation before a direct merge;
- direct GraphQL Queue enqueue;
- Queue lookup before a pull request has an active entry;
- GraphQL `PullRequest.isMergeQueueEnabled`.

Existing active Queue entries remain authoritative: their persisted Queue ID is
read first, so this change does not rewrite or move in-flight entries.

GraphQL resolves the compact target once per pull request and reuses it for
Queue lookup and `isMergeQueueEnabled`. A query that asks only for the enabled
flag does not load or create the Queue view.

## Verification

- Service regression: a three-layer Stack whose Queue exists only on `main`
  enters that Queue through async Stack merge submission.
- GraphQL regression: before queueing, the upper Stack PR reports
  `isMergeQueueEnabled: true`, resolves the Queue for `main`, and performs one
  compact Stack target query.
- Concurrency regression: direct enqueue and unstack serialize on the shared
  topology lock, preserving one three-member Queue batch and the Stack.
- Lifecycle regression: expired pending, merging, and cascading timestamps do
  not hide durable work from recovery or release protected Stack topology;
  recovery renews visibility and converges the request to a terminal result.
- Existing Stack dequeue coverage continues to exercise direct Queue enqueue.
