@@ -370,6 +469,49 @@ export function CIAutomationHeader({
);
}
+export function CIAutomationQueueStatusRow({
+ queueState,
+}: {
+ queueState: CIAutomationQueueRecoveryState;
+}) {
+ const { t } = useTranslation();
+ if (queueState.status === "none") return null;
+ let message: string;
+ switch (queueState.status) {
+ case "queued":
+ message = t("github:mergeQueueRecoveryStatusQueued");
+ break;
+ case "removed_actionable":
+ message = t("github:mergeQueueRecoveryStatusActionable");
+ break;
+ case "removed_not_actionable":
+ message = t("github:mergeQueueRecoveryStatusNotActionable");
+ break;
+ case "repair_requested":
+ if (queueState.waitingForCommit) {
+ message = t("github:mergeQueueRecoveryStatusRepairRequestedWaiting");
+ } else {
+ message = t("github:mergeQueueRecoveryStatusRepairRequested");
+ }
+ break;
+ case "waiting_for_commit":
+ message = t("github:mergeQueueRecoveryStatusWaitingForCommit");
+ break;
+ case "waiting_for_checks":
+ message = t("github:mergeQueueRecoveryStatusWaitingForChecks");
+ break;
+ }
+ return (
+
+ {message}
+
+ );
+}
+
export function CIAutomationOptionRows({
pr,
prOptions,
@@ -387,6 +529,13 @@ export function CIAutomationOptionRows({
}) {
const { t } = useTranslation();
const scopeKey = prAutomationScopeKey(pr);
+ const queueState = deriveCIAutomationQueueState(pr, prOptions, automationState);
+ const autoFixSupportingText = autoFixQueueSupport(t, queueState, prOptions.auto_fix_enabled);
+ const autoMergeSupportingText = autoMergeQueueSupport(
+ t,
+ queueState,
+ prOptions.auto_merge_enabled,
+ );
return (
<>
patchOption({ auto_fix_enabled: checked })}
+ supportingText={autoFixSupportingText}
help={
prOptions.auto_fix_enabled ? (
@@ -403,11 +553,13 @@ export function CIAutomationOptionRows({
/>
patchOption({ auto_merge_enabled: checked })}
+ supportingText={autoMergeSupportingText}
/>
+
= {}): TaskPR {
};
}
-function renderPopover() {
+function renderPopover(pr: TaskPR = makePR()) {
return render(
-
+
,
@@ -305,6 +305,73 @@ describe("PRCIPopover automation status", () => {
expect(screen.queryByLabelText(/edit.*review/i)).toBeNull();
});
+ it("shows active queue context without adding another switch", () => {
+ renderPopover(
+ makePR({
+ merge_queue_state: "queued",
+ merge_queue_entry_id: "entry-a",
+ head_sha: "head-a",
+ }),
+ );
+
+ expect(screen.getByText("Merge queue automation")).not.toBeNull();
+ expect(screen.getByText("PR #1 is in the merge queue")).not.toBeNull();
+ expect(screen.getByTestId("ci-merge-queue-recovery-status").textContent).toContain(
+ "Active merge queue attempt",
+ );
+ expect(screen.getAllByRole("switch")).toHaveLength(2);
+ });
+
+ it("shows classified recovery state and generic copy for unknown causes", () => {
+ hookMocks.options = makeOptions({
+ pr_states: [
+ {
+ task_id: "task-1",
+ repository_id: "",
+ pr_number: 1,
+ last_fix_signature: "",
+ last_fix_checkpoint_json: "",
+ last_fix_enqueued_at: null,
+ last_fix_session_id: null,
+ auto_fix_round_count: 0,
+ auto_fix_exhausted_at: null,
+ last_merge_signature: "",
+ last_merge_attempt_at: null,
+ last_queue_attempt_head_sha: "head-a",
+ last_queue_fix_event_id: "",
+ last_queue_removal_cause: "provider_changed",
+ last_error: null,
+ created_at: "",
+ updated_at: "",
+ },
+ ],
+ });
+ renderPopover(
+ makePR({
+ head_sha: "head-a",
+ merge_queue_last_removal_id: "removal-a",
+ merge_queue_last_removal_reason: "provider changed this",
+ }),
+ );
+
+ expect(screen.getByText("Merge queue recovery")).not.toBeNull();
+ expect(screen.getByText("PR #1 was removed from the merge queue")).not.toBeNull();
+ expect(screen.getByTestId("ci-merge-queue-recovery-status").textContent).toContain(
+ "No automatic repair",
+ );
+ expect(screen.queryByText("provider changed this")).toBeNull();
+ });
+});
+
+describe("PRCIPopover linked PR status", () => {
+ beforeEach(() => {
+ resetHookMocks();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
it("uses the PR title in the header and omits the redundant detail link", () => {
renderPopover();
diff --git a/apps/web/components/github/pr-status-chip.test.tsx b/apps/web/components/github/pr-status-chip.test.tsx
index e616f85023..8ceb98094c 100644
--- a/apps/web/components/github/pr-status-chip.test.tsx
+++ b/apps/web/components/github/pr-status-chip.test.tsx
@@ -533,7 +533,7 @@ describe("PRStatusChip CI automation mobile parity", () => {
const drawer = document.querySelector(DRAWER_SELECTOR);
expect(drawer?.textContent).toContain("Auto-fix CI and address comments");
- expect(drawer?.textContent).toContain("Auto-merge when ready");
+ expect(drawer?.textContent).toContain("Auto-merge or requeue when ready");
act(() => {
fireEvent.click(screen.getByLabelText("Edit auto-fix prompt for this task"));
diff --git a/apps/web/e2e/helpers/api-client.ts b/apps/web/e2e/helpers/api-client.ts
index 8c26851b11..ec0ee8c11d 100644
--- a/apps/web/e2e/helpers/api-client.ts
+++ b/apps/web/e2e/helpers/api-client.ts
@@ -1498,6 +1498,42 @@ export class ApiClient {
});
}
+ async mockGitHubTransitionMergeQueue(data: {
+ task_id: string;
+ owner: string;
+ repo: string;
+ pr_number: number;
+ head_sha?: string;
+ merge_queue_state?: string;
+ merge_queue_position?: number | null;
+ merge_queue_entry_id?: string;
+ merge_queue_entry_head_sha?: string;
+ merge_queue_estimated_time_to_merge_seconds?: number | null;
+ merge_queue_last_removal_id?: string;
+ merge_queue_last_removed_at?: string;
+ merge_queue_last_removal_reason?: string;
+ merge_queue_last_removal_before_sha?: string;
+ checks?: Array<{
+ name: string;
+ source?: string;
+ status?: string;
+ conclusion?: string;
+ html_url?: string;
+ output?: string;
+ }>;
+ }): Promise {
+ await this.request("PUT", "/api/v1/github/mock/merge-queue", data);
+ }
+
+ async mockGitHubGetMergeAttempts(): Promise<
+ Array<{ owner: string; repo: string; number: number; merge_method: string }>
+ > {
+ const response = await this.request<{
+ attempts?: Array<{ owner: string; repo: string; number: number; merge_method: string }>;
+ }>("GET", "/api/v1/github/mock/merge-attempts");
+ return response.attempts ?? [];
+ }
+
async mockGitHubAddIssues(issues: MockIssue[]): Promise {
await this.request("POST", "/api/v1/github/mock/issues", { issues });
await this.seedMockGitHubRepositoryAccess(issues);
@@ -1679,12 +1715,19 @@ export class ApiClient {
base_branch: string;
author_login: string;
state?: string;
+ head_sha?: string;
review_state?: string;
checks_state?: string;
mergeable_state?: string;
merge_queue_state?: string;
merge_queue_position?: number | null;
+ merge_queue_entry_id?: string;
+ merge_queue_entry_head_sha?: string;
merge_queue_estimated_time_to_merge_seconds?: number | null;
+ merge_queue_last_removal_id?: string;
+ merge_queue_last_removed_at?: string;
+ merge_queue_last_removal_reason?: string;
+ merge_queue_last_removal_before_sha?: string;
additions?: number;
deletions?: number;
review_count?: number;
diff --git a/apps/web/e2e/tests/pr/ci-automation-options.spec.ts b/apps/web/e2e/tests/pr/ci-automation-options.spec.ts
index 449ea2f8ce..59672438cb 100644
--- a/apps/web/e2e/tests/pr/ci-automation-options.spec.ts
+++ b/apps/web/e2e/tests/pr/ci-automation-options.spec.ts
@@ -189,7 +189,9 @@ test.describe("PR CI automation options", () => {
await expect(
popover.getByRole("switch", { name: "Auto-fix CI and address comments" }),
).toBeVisible();
- await expect(popover.getByRole("switch", { name: "Auto-merge when ready" })).toBeVisible();
+ await expect(
+ popover.getByRole("switch", { name: "Auto-merge or requeue when ready" }),
+ ).toBeVisible();
const reviewFollowUp = popover.getByTestId("ci-review-follow-up-trigger");
await expect(reviewFollowUp).toHaveAttribute("aria-expanded", "false");
await reviewFollowUp.click();
@@ -211,7 +213,7 @@ test.describe("PR CI automation options", () => {
).toBeVisible();
await popover.getByRole("switch", { name: "Auto-fix CI and address comments" }).click();
- await popover.getByRole("switch", { name: "Auto-merge when ready" }).click();
+ await popover.getByRole("switch", { name: "Auto-merge or requeue when ready" }).click();
await popover.getByRole("switch", { name: "Your review is requested" }).click();
await popover.getByRole("switch", { name: "PR merged" }).click();
@@ -225,11 +227,17 @@ test.describe("PR CI automation options", () => {
});
await popover.getByLabel("Explain CI automation options").hover();
- await expect(testPage.getByText(/1 minute PR refresh loop/)).toBeVisible();
- await expect(testPage.getByText(/notification switches wake the task's agent/)).toBeVisible();
- await expect(
- testPage.getByText(/workspace's connected GitHub account is requested for review/i),
- ).toBeVisible();
+ const queueRecoveryHelp = testPage.getByRole("tooltip");
+ await expect(queueRecoveryHelp).toContainText("Auto-fix repairs actionable queue removals.");
+ await expect(queueRecoveryHelp).toContainText(
+ "Auto-merge submits an eligible head or requeues it after a new commit.",
+ );
+ await expect(queueRecoveryHelp).toContainText(
+ "Both controls form the repair and requeue loop.",
+ );
+ await expect(queueRecoveryHelp).toContainText(
+ "Kandev never requeues the same head after removal.",
+ );
await openPromptDialog(session);
const promptDialog = testPage.getByRole("dialog", { name: "Auto-fix prompt" });
@@ -264,7 +272,7 @@ test.describe("PR CI automation options", () => {
}),
).toBeChecked();
await expect(
- reloaded.prTopbarPopover().getByRole("switch", { name: "Auto-merge when ready" }),
+ reloaded.prTopbarPopover().getByRole("switch", { name: "Auto-merge or requeue when ready" }),
).toBeChecked();
});
@@ -303,7 +311,7 @@ test.describe("PR CI automation options", () => {
popover.getByRole("switch", { name: "Auto-fix CI and address comments" }),
).toBeVisible();
await popover.getByRole("switch", { name: "Auto-fix CI and address comments" }).click();
- await popover.getByRole("switch", { name: "Auto-merge when ready" }).click();
+ await popover.getByRole("switch", { name: "Auto-merge or requeue when ready" }).click();
await expect
.poll(async () => apiClient.getTaskCIAutomationOptions(taskId))
@@ -327,7 +335,9 @@ test.describe("PR CI automation options", () => {
await expect(
popover.getByRole("switch", { name: "Auto-fix CI and address comments" }),
).not.toBeChecked();
- await expect(popover.getByRole("switch", { name: "Auto-merge when ready" })).not.toBeChecked();
+ await expect(
+ popover.getByRole("switch", { name: "Auto-merge or requeue when ready" }),
+ ).not.toBeChecked();
// Reload: independence must persist across a full page load. Select each
// tab explicitly — the default tab tracks live "worst status" data (real
@@ -340,14 +350,14 @@ test.describe("PR CI automation options", () => {
reloadedPopover.getByRole("switch", { name: "Auto-fix CI and address comments" }),
).toBeChecked();
await expect(
- reloadedPopover.getByRole("switch", { name: "Auto-merge when ready" }),
+ reloadedPopover.getByRole("switch", { name: "Auto-merge or requeue when ready" }),
).toBeChecked();
await reloadedPopover.getByRole("tab", { name: `${REPO} #${secondPRNumber}` }).click();
await expect(
reloadedPopover.getByRole("switch", { name: "Auto-fix CI and address comments" }),
).not.toBeChecked();
await expect(
- reloadedPopover.getByRole("switch", { name: "Auto-merge when ready" }),
+ reloadedPopover.getByRole("switch", { name: "Auto-merge or requeue when ready" }),
).not.toBeChecked();
});
@@ -367,4 +377,174 @@ test.describe("PR CI automation options", () => {
"Lifecycle prompt could not be delivered to a task session.",
);
});
+
+ test("desktop merge queue recovery proves repair and new-head requeue", async ({
+ testPage,
+ apiClient,
+ seedData,
+ }) => {
+ test.setTimeout(120_000);
+ const queuedHead = "head-queued-desktop";
+ const replacementHead = "head-replacement-desktop";
+ const taskId = await seedTaskWithPR(apiClient, seedData, "CI merge queue recovery", {
+ head_sha: queuedHead,
+ checks_state: "success",
+ checks_total: 1,
+ checks_passing: 1,
+ unresolved_review_threads: 0,
+ mergeable_state: "clean",
+ merge_queue_state: "queued",
+ merge_queue_position: 1,
+ merge_queue_entry_id: "entry-desktop-a",
+ merge_queue_entry_head_sha: queuedHead,
+ });
+ await apiClient.mockGitHubSetMergeOutcome(OWNER, REPO, PR_NUMBER, "queued");
+
+ const session = await openTask(testPage, taskId);
+ const popover = session.prTopbarPopover();
+ await expect(popover.getByText("Merge queue automation")).toBeVisible();
+ await expect(popover.getByText("PR #144 is in the merge queue")).toBeVisible();
+ await expect(popover.getByTestId("ci-merge-queue-recovery-status")).toContainText(
+ "Active merge queue attempt",
+ );
+ await expect(popover.getByRole("switch")).toHaveCount(2);
+
+ await popover.getByRole("switch", { name: "Auto-fix CI and address comments" }).click();
+ await popover.getByRole("switch", { name: "Auto-merge or requeue when ready" }).click();
+ await expect
+ .poll(async () => apiClient.getTaskCIAutomationOptions(taskId))
+ .toMatchObject({
+ pr_options: expect.arrayContaining([
+ expect.objectContaining({
+ pr_number: PR_NUMBER,
+ auto_fix_enabled: true,
+ auto_merge_enabled: true,
+ }),
+ ]),
+ });
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(0);
+
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: queuedHead,
+ merge_queue_state: "",
+ merge_queue_entry_id: "",
+ merge_queue_entry_head_sha: "",
+ merge_queue_last_removal_id: "removal-desktop-a",
+ merge_queue_last_removed_at: new Date().toISOString(),
+ merge_queue_last_removal_reason: "checks failed on merge group",
+ merge_queue_last_removal_before_sha: "merge-group-desktop-a",
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "failure",
+ html_url: "https://example.test/checks/merge-group-desktop",
+ },
+ ],
+ });
+
+ await expect
+ .poll(async () => {
+ const options = await apiClient.getTaskCIAutomationOptions(taskId);
+ const state = options.pr_states?.find((item) => item.pr_number === PR_NUMBER);
+ return {
+ round: state?.auto_fix_round_count,
+ event: state?.last_queue_fix_event_id,
+ cause: state?.last_queue_removal_cause,
+ };
+ })
+ .toEqual({ round: 1, event: "removal-desktop-a", cause: "checks_failed" });
+ await expect(popover.getByText("Merge queue recovery")).toBeVisible();
+ await expect(popover.getByText("PR #144 was removed: checks failed")).toBeVisible();
+ await expect(popover.getByTestId("ci-merge-queue-recovery-status")).toContainText(
+ "Repair requested. Waiting for a new commit before requeue",
+ );
+
+ // A clean replacement of the same head is still blocked by the durable
+ // queue-attempt head guard, so it cannot create a merge request.
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: queuedHead,
+ merge_queue_state: "",
+ merge_queue_entry_id: "",
+ merge_queue_entry_head_sha: "",
+ merge_queue_last_removal_id: "removal-desktop-a",
+ merge_queue_last_removal_reason: "checks failed on merge group",
+ merge_queue_last_removal_before_sha: "merge-group-desktop-a",
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "success",
+ html_url: "https://example.test/checks/merge-group-desktop",
+ },
+ ],
+ });
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(0);
+
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: replacementHead,
+ merge_queue_state: "",
+ merge_queue_entry_id: "",
+ merge_queue_entry_head_sha: "",
+ merge_queue_last_removal_id: "removal-desktop-a",
+ merge_queue_last_removal_reason: "checks failed on merge group",
+ merge_queue_last_removal_before_sha: "merge-group-desktop-a",
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "success",
+ html_url: "https://example.test/checks/merge-group-desktop",
+ },
+ ],
+ });
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(1);
+ await expect
+ .poll(async () => apiClient.getTaskCIAutomationOptions(taskId))
+ .toMatchObject({
+ pr_states: expect.arrayContaining([
+ expect.objectContaining({ last_queue_attempt_head_sha: replacementHead }),
+ ]),
+ });
+
+ // Reflect GitHub accepting the queued merge request. The old removal
+ // evidence remains durable, but the active entry takes presentation
+ // precedence and a second merge request is still prohibited.
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: replacementHead,
+ merge_queue_state: "queued",
+ merge_queue_position: 1,
+ merge_queue_entry_id: "entry-desktop-b",
+ merge_queue_entry_head_sha: replacementHead,
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "success",
+ html_url: "https://example.test/checks/merge-group-desktop",
+ },
+ ],
+ });
+ await expect(popover.getByText("Merge queue automation")).toBeVisible();
+ await expect(popover.getByTestId("ci-merge-queue-recovery-status")).toContainText(
+ "Active merge queue attempt",
+ );
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(1);
+ });
});
diff --git a/apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts b/apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts
index 4e5eda032c..9eb8294260 100644
--- a/apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts
+++ b/apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts
@@ -16,7 +16,12 @@ function manyRunningChecks(count: number) {
}));
}
-async function seedTaskWithPR(apiClient: ApiClient, seedData: SeedData, title: string) {
+async function seedTaskWithPR(
+ apiClient: ApiClient,
+ seedData: SeedData,
+ title: string,
+ prOverrides: Partial[0]> = {},
+) {
await apiClient.mockGitHubReset();
await apiClient.mockGitHubSetUser("test-user");
const task = await apiClient.createTaskWithAgent(
@@ -47,6 +52,7 @@ async function seedTaskWithPR(apiClient: ApiClient, seedData: SeedData, title: s
checks_state: "failure",
checks_total: 2,
checks_passing: 1,
+ ...prOverrides,
});
return task.id;
}
@@ -120,7 +126,9 @@ test.describe("mobile PR CI automation options", () => {
await expect(
drawer.getByRole("switch", { name: "Auto-fix CI and address comments" }),
).toBeVisible();
- await expect(drawer.getByRole("switch", { name: "Auto-merge when ready" })).toBeVisible();
+ await expect(
+ drawer.getByRole("switch", { name: "Auto-merge or requeue when ready" }),
+ ).toBeVisible();
const reviewFollowUp = drawer.getByTestId("ci-review-follow-up-trigger");
await expect(reviewFollowUp).toHaveAttribute("aria-expanded", "false");
await expect(reviewFollowUp).toHaveCSS("min-height", "44px");
@@ -274,4 +282,196 @@ test.describe("mobile PR CI automation options", () => {
expect(scrollMetrics.scrollHeight).toBeGreaterThan(scrollMetrics.clientHeight);
expect(documentMetrics.scrollWidth).toBeLessThanOrEqual(documentMetrics.clientWidth);
});
+
+ test("mobile merge queue recovery proves repair and new-head requeue", async ({
+ testPage,
+ apiClient,
+ seedData,
+ }) => {
+ test.setTimeout(120_000);
+ const queuedHead = "head-queued-mobile";
+ const replacementHead = "head-replacement-mobile";
+ const taskId = await seedTaskWithPR(apiClient, seedData, "CI mobile merge queue recovery", {
+ head_sha: queuedHead,
+ checks_state: "success",
+ checks_total: 1,
+ checks_passing: 1,
+ unresolved_review_threads: 0,
+ mergeable_state: "clean",
+ merge_queue_state: "queued",
+ merge_queue_position: 1,
+ merge_queue_entry_id: "entry-mobile-a",
+ merge_queue_entry_head_sha: queuedHead,
+ });
+ await apiClient.mockGitHubSetMergeOutcome(OWNER, REPO, PR_NUMBER, "queued");
+ await interceptTallPRFeedback(testPage);
+
+ await testPage.goto(`/t/${taskId}`);
+ const session = new SessionPage(testPage);
+ await session.waitForLoad();
+ await session.tapPRStatusChip();
+ const drawer = session.prStatusChipDrawer();
+ await expect(drawer.getByText("Merge queue automation")).toBeVisible();
+ await expect(drawer.getByText("PR #144 is in the merge queue")).toBeVisible();
+ await expect(drawer.getByTestId("ci-merge-queue-recovery-status")).toContainText(
+ "Active merge queue attempt",
+ );
+ await expect(drawer.getByRole("switch")).toHaveCount(2);
+ await expect(
+ drawer.getByRole("switch", { name: "Auto-fix CI and address comments" }).locator(".."),
+ ).toHaveCSS("min-height", "44px");
+ await expect(
+ drawer.getByRole("switch", { name: "Auto-merge or requeue when ready" }).locator(".."),
+ ).toHaveCSS("min-height", "44px");
+
+ await drawer.getByRole("switch", { name: "Auto-fix CI and address comments" }).tap();
+ await drawer.getByRole("switch", { name: "Auto-merge or requeue when ready" }).tap();
+ await expect
+ .poll(async () => apiClient.getTaskCIAutomationOptions(taskId))
+ .toMatchObject({
+ pr_options: expect.arrayContaining([
+ expect.objectContaining({
+ pr_number: PR_NUMBER,
+ auto_fix_enabled: true,
+ auto_merge_enabled: true,
+ }),
+ ]),
+ });
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(0);
+
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: queuedHead,
+ merge_queue_state: "",
+ merge_queue_entry_id: "",
+ merge_queue_entry_head_sha: "",
+ merge_queue_last_removal_id: "removal-mobile-a",
+ merge_queue_last_removed_at: new Date().toISOString(),
+ merge_queue_last_removal_reason: "checks failed on merge group",
+ merge_queue_last_removal_before_sha: "merge-group-mobile-a",
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "failure",
+ html_url: "https://example.test/checks/merge-group-mobile",
+ },
+ ],
+ });
+ await expect
+ .poll(async () => {
+ const options = await apiClient.getTaskCIAutomationOptions(taskId);
+ const state = options.pr_states?.find((item) => item.pr_number === PR_NUMBER);
+ return {
+ round: state?.auto_fix_round_count,
+ event: state?.last_queue_fix_event_id,
+ cause: state?.last_queue_removal_cause,
+ };
+ })
+ .toEqual({ round: 1, event: "removal-mobile-a", cause: "checks_failed" });
+ await expect(drawer.getByText("Merge queue recovery")).toBeVisible();
+ await expect(drawer.getByText("PR #144 was removed: checks failed")).toBeVisible();
+ await expect(drawer.getByTestId("ci-merge-queue-recovery-status")).toContainText(
+ "Repair requested. Waiting for a new commit before requeue",
+ );
+
+ const scrollBody = drawer.locator("[data-vaul-no-drag]");
+ const [drawerBox, scrollMetrics, documentMetrics] = await Promise.all([
+ drawer.boundingBox(),
+ scrollBody.evaluate((element) => ({
+ clientHeight: element.clientHeight,
+ scrollHeight: element.scrollHeight,
+ })),
+ testPage.evaluate(() => ({
+ clientWidth: document.documentElement.clientWidth,
+ scrollWidth: document.documentElement.scrollWidth,
+ })),
+ ]);
+ expect(drawerBox).not.toBeNull();
+ expect(drawerBox!.x).toBeGreaterThanOrEqual(0);
+ expect(drawerBox!.x + drawerBox!.width).toBeLessThanOrEqual(documentMetrics.clientWidth);
+ expect(scrollMetrics.scrollHeight).toBeGreaterThan(scrollMetrics.clientHeight);
+ expect(documentMetrics.scrollWidth).toBeLessThanOrEqual(documentMetrics.clientWidth);
+
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: queuedHead,
+ merge_queue_state: "",
+ merge_queue_entry_id: "",
+ merge_queue_entry_head_sha: "",
+ merge_queue_last_removal_id: "removal-mobile-a",
+ merge_queue_last_removal_reason: "checks failed on merge group",
+ merge_queue_last_removal_before_sha: "merge-group-mobile-a",
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "success",
+ html_url: "https://example.test/checks/merge-group-mobile",
+ },
+ ],
+ });
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(0);
+
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: replacementHead,
+ merge_queue_state: "",
+ merge_queue_entry_id: "",
+ merge_queue_entry_head_sha: "",
+ merge_queue_last_removal_id: "removal-mobile-a",
+ merge_queue_last_removal_reason: "checks failed on merge group",
+ merge_queue_last_removal_before_sha: "merge-group-mobile-a",
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "success",
+ html_url: "https://example.test/checks/merge-group-mobile",
+ },
+ ],
+ });
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(1);
+ await expect
+ .poll(async () => apiClient.getTaskCIAutomationOptions(taskId))
+ .toMatchObject({
+ pr_states: expect.arrayContaining([
+ expect.objectContaining({ last_queue_attempt_head_sha: replacementHead }),
+ ]),
+ });
+
+ await apiClient.mockGitHubTransitionMergeQueue({
+ task_id: taskId,
+ owner: OWNER,
+ repo: REPO,
+ pr_number: PR_NUMBER,
+ head_sha: replacementHead,
+ merge_queue_state: "queued",
+ merge_queue_position: 1,
+ merge_queue_entry_id: "entry-mobile-b",
+ merge_queue_entry_head_sha: replacementHead,
+ checks: [
+ {
+ name: "merge group checks",
+ status: "completed",
+ conclusion: "success",
+ html_url: "https://example.test/checks/merge-group-mobile",
+ },
+ ],
+ });
+ await expect(drawer.getByText("Merge queue automation")).toBeVisible();
+ await expect(drawer.getByTestId("ci-merge-queue-recovery-status")).toContainText(
+ "Active merge queue attempt",
+ );
+ await expect.poll(() => apiClient.mockGitHubGetMergeAttempts()).toHaveLength(1);
+ });
});
diff --git a/apps/web/e2e/tests/pr/mobile-pr-ci-chip.spec.ts b/apps/web/e2e/tests/pr/mobile-pr-ci-chip.spec.ts
index 889fbef782..98506558cc 100644
--- a/apps/web/e2e/tests/pr/mobile-pr-ci-chip.spec.ts
+++ b/apps/web/e2e/tests/pr/mobile-pr-ci-chip.spec.ts
@@ -172,7 +172,9 @@ test.describe("mobile PR CI chip drawer", () => {
await expect(
drawer.getByRole("switch", { name: "Auto-fix CI and address comments" }),
).toBeVisible();
- await expect(drawer.getByRole("switch", { name: "Auto-merge when ready" })).toBeVisible();
+ await expect(
+ drawer.getByRole("switch", { name: "Auto-merge or requeue when ready" }),
+ ).toBeVisible();
await expect(drawer.getByRole("switch", { name: "Your review is requested" })).toBeVisible();
await expect(drawer.getByRole("switch", { name: "PR merged" })).toBeVisible();
await expect(drawer.getByRole("switch", { name: "PR closed without merging" })).toBeVisible();
diff --git a/apps/web/lib/github/ci-automation.test.ts b/apps/web/lib/github/ci-automation.test.ts
index 4485064ce9..8b842be599 100644
--- a/apps/web/lib/github/ci-automation.test.ts
+++ b/apps/web/lib/github/ci-automation.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
autoFixRoundForState,
clampAutoFixRound,
+ deriveCIAutomationQueueState,
findCIAutomationStateForPR,
findPRAutomationOptionsForPR,
normalizeAutoFixMaxRounds,
@@ -132,3 +133,95 @@ describe("CI automation helpers", () => {
expect(clampAutoFixRound(14, 10)).toBe(10);
});
});
+
+describe("CI merge queue recovery helpers", () => {
+ function makeQueuePR(overrides: Partial = {}): TaskPR {
+ return { ...makePR("", 42), ...overrides };
+ }
+
+ it("prioritizes an active queue entry", () => {
+ const result = deriveCIAutomationQueueState(
+ makeQueuePR({
+ state: "open",
+ merge_queue_state: "queued",
+ merge_queue_entry_id: "entry-a",
+ head_sha: "head-a",
+ }),
+ makePRAutomationOptions("repo-1", 42),
+ undefined,
+ );
+
+ expect(result.status).toBe("queued");
+ expect(result.context).toBe("queued");
+ });
+
+ it("shows an actionable removal until one repair round is accepted", () => {
+ const pr = makeQueuePR({
+ pr_number: 42,
+ head_sha: "head-a",
+ merge_queue_last_removal_id: "removal-a",
+ });
+ const options = makePRAutomationOptions("", 42);
+ const result = deriveCIAutomationQueueState(pr, options, {
+ last_queue_removal_cause: "checks_failed",
+ } as TaskCIPRAutomationState);
+
+ expect(result.status).toBe("removed_actionable");
+ expect(result.removalCause).toBe("checks_failed");
+ expect(result.repairAccepted).toBe(false);
+ });
+
+ it("uses generic copy state for unknown removals", () => {
+ const result = deriveCIAutomationQueueState(
+ makeQueuePR({ pr_number: 42, head_sha: "head-a", merge_queue_last_removal_id: "removal-a" }),
+ makePRAutomationOptions("", 42),
+ { last_queue_removal_cause: "provider_changed" } as TaskCIPRAutomationState,
+ );
+
+ expect(result.status).toBe("removed_not_actionable");
+ expect(result.removalCause).toBe("unknown");
+ });
+
+ it("classifies the persisted removal reason when CI state is absent", () => {
+ const result = deriveCIAutomationQueueState(
+ makeQueuePR({
+ pr_number: 42,
+ head_sha: "head-a",
+ merge_queue_last_removal_id: "removal-a",
+ merge_queue_last_removal_reason: "CI checks failed on merge group",
+ }),
+ makePRAutomationOptions("", 42),
+ undefined,
+ );
+
+ expect(result.status).toBe("removed_actionable");
+ expect(result.removalCause).toBe("checks_failed");
+ });
+
+ it("distinguishes accepted repair, same-head wait, and pending new-head checks", () => {
+ const pr = makeQueuePR({
+ pr_number: 42,
+ head_sha: "head-a",
+ merge_queue_last_removal_id: "removal-a",
+ });
+ const options = makePRAutomationOptions("", 42, { auto_merge_enabled: true });
+ const accepted = deriveCIAutomationQueueState(pr, options, {
+ last_queue_attempt_head_sha: "head-a",
+ last_queue_fix_event_id: "removal-a",
+ last_queue_removal_cause: "checks_failed",
+ } as TaskCIPRAutomationState);
+
+ expect(accepted.status).toBe("repair_requested");
+ expect(accepted.waitingForCommit).toBe(true);
+
+ const pending = deriveCIAutomationQueueState(
+ { ...pr, head_sha: "head-b", checks_state: "pending" },
+ options,
+ {
+ last_queue_attempt_head_sha: "head-a",
+ last_queue_removal_cause: "checks_failed",
+ } as TaskCIPRAutomationState,
+ );
+ expect(pending.status).toBe("waiting_for_checks");
+ });
+});
diff --git a/apps/web/lib/github/ci-automation.ts b/apps/web/lib/github/ci-automation.ts
index ff0676168d..5e1c4be2c6 100644
--- a/apps/web/lib/github/ci-automation.ts
+++ b/apps/web/lib/github/ci-automation.ts
@@ -1,4 +1,9 @@
-import type { TaskCIPRAutomationState, TaskPR, TaskPRAutomationOptions } from "@/lib/types/github";
+import type {
+ CIAutomationQueueRemovalCause,
+ TaskCIPRAutomationState,
+ TaskPR,
+ TaskPRAutomationOptions,
+} from "@/lib/types/github";
const DEFAULT_AUTO_FIX_MAX_ROUNDS = 10;
@@ -13,6 +18,220 @@ export const DISABLED_PR_AUTOMATION_OPTIONS: Omit<
prompt_on_closed: false,
};
+export type CIAutomationQueueStatus =
+ | "none"
+ | "queued"
+ | "removed_actionable"
+ | "removed_not_actionable"
+ | "repair_requested"
+ | "waiting_for_commit"
+ | "waiting_for_checks";
+
+export type CIAutomationQueueRecoveryState = {
+ context: "normal" | "queued" | "recovery";
+ status: CIAutomationQueueStatus;
+ removalCause: CIAutomationQueueRemovalCause;
+ repairAccepted: boolean;
+ waitingForCommit: boolean;
+};
+
+const ACTIONABLE_QUEUE_REMOVAL_CAUSES: CIAutomationQueueRemovalCause[] = [
+ "checks_failed",
+ "checks_timed_out",
+ "conflict",
+];
+
+function normalizedQueueRemovalReason(value: string | undefined): string {
+ return (value ?? "")
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "_")
+ .replace(/^_+|_+$/g, "");
+}
+
+const QUEUE_REMOVAL_CAUSE_ALIASES: Record = {
+ checks_failed: "checks_failed",
+ checks_timed_out: "checks_timed_out",
+ conflict: "conflict",
+ manual: "manual",
+ branch_protection: "branch_protection",
+ check_failed: "checks_failed",
+ checks_failure: "checks_failed",
+ ci_checks_failed: "checks_failed",
+ ci_check_failed: "checks_failed",
+ checks_failed_on_merge_group: "checks_failed",
+ checks_failed_on_merge_queue: "checks_failed",
+ ci_checks_failed_on_merge_group: "checks_failed",
+ ci_checks_failed_on_merge_queue: "checks_failed",
+ check_timed_out: "checks_timed_out",
+ checks_timeout: "checks_timed_out",
+ timeout: "checks_timed_out",
+ timed_out: "checks_timed_out",
+ checks_timed_out_on_merge_group: "checks_timed_out",
+ merge_conflict: "conflict",
+ merge_conflicts: "conflict",
+ unmergeable: "conflict",
+ removed_manually: "manual",
+ user_removed: "manual",
+ branch_protection_failed: "branch_protection",
+ required_branch_protection: "branch_protection",
+};
+
+function normalizeQueueRemovalCause(
+ value: string | undefined,
+ pr?: TaskPR,
+): CIAutomationQueueRemovalCause {
+ const normalized = normalizedQueueRemovalReason(value);
+ const knownCause = QUEUE_REMOVAL_CAUSE_ALIASES[normalized];
+ if (knownCause) {
+ return knownCause;
+ }
+ if (pr) {
+ const mergeableState = pr.mergeable_state?.trim().toLowerCase() ?? "";
+ const queueState = pr.merge_queue_state?.trim().toLowerCase() ?? "";
+ if (
+ mergeableState === "dirty" ||
+ mergeableState.includes("conflict") ||
+ queueState === "unmergeable" ||
+ queueState.includes("unmergeable")
+ ) {
+ return "conflict";
+ }
+ }
+ return "unknown";
+}
+
+function hasActiveQueueEntry(pr: TaskPR): boolean {
+ return pr.state === "open" && Boolean(pr.merge_queue_state?.trim());
+}
+
+function isReadyForMerge(pr: TaskPR): boolean {
+ if (pr.state !== "open" || pr.checks_state !== "success" || pr.mergeable_state !== "clean") {
+ return false;
+ }
+ if (
+ pr.review_state === "changes_requested" ||
+ pr.pending_review_count > 0 ||
+ pr.unresolved_review_threads > 0
+ ) {
+ return false;
+ }
+ return pr.required_reviews == null || pr.review_count >= pr.required_reviews;
+}
+
+function queueStateForRemoval(
+ pr: TaskPR,
+ options: Pick,
+ state: TaskCIPRAutomationState | undefined,
+ removalID: string,
+): CIAutomationQueueRecoveryState {
+ const stateCause = normalizeQueueRemovalCause(state?.last_queue_removal_cause);
+ const removalCause =
+ stateCause !== "unknown"
+ ? stateCause
+ : normalizeQueueRemovalCause(pr.merge_queue_last_removal_reason, pr);
+ const actionable = ACTIONABLE_QUEUE_REMOVAL_CAUSES.includes(removalCause);
+ const headContext = queueHeadContext(pr, state, removalID);
+
+ if (headContext.repairAccepted) {
+ return {
+ context: "recovery",
+ status: "repair_requested",
+ removalCause,
+ repairAccepted: true,
+ waitingForCommit: options.auto_merge_enabled,
+ };
+ }
+ if (options.auto_merge_enabled && headContext.sameHead) {
+ return {
+ context: "recovery",
+ status: "waiting_for_commit",
+ removalCause,
+ repairAccepted: false,
+ waitingForCommit: true,
+ };
+ }
+ if (shouldWaitForNewHead(pr, options, headContext)) {
+ return {
+ context: "recovery",
+ status: "waiting_for_checks",
+ removalCause,
+ repairAccepted: false,
+ waitingForCommit: false,
+ };
+ }
+ return {
+ context: "recovery",
+ status: actionable ? "removed_actionable" : "removed_not_actionable",
+ removalCause,
+ repairAccepted: false,
+ waitingForCommit: false,
+ };
+}
+
+function queueHeadContext(
+ pr: TaskPR,
+ state: TaskCIPRAutomationState | undefined,
+ removalID: string,
+): {
+ currentHead: string;
+ lastAttemptHead: string;
+ sameHead: boolean;
+ repairAccepted: boolean;
+} {
+ const currentHead = pr.head_sha?.trim() ?? "";
+ const lastAttemptHead = state?.last_queue_attempt_head_sha?.trim() ?? "";
+ const sameHead = Boolean(currentHead && lastAttemptHead && currentHead === lastAttemptHead);
+ return {
+ currentHead,
+ lastAttemptHead,
+ sameHead,
+ repairAccepted: state?.last_queue_fix_event_id === removalID && sameHead,
+ };
+}
+
+function shouldWaitForNewHead(
+ pr: TaskPR,
+ options: Pick,
+ headContext: ReturnType,
+): boolean {
+ return Boolean(
+ options.auto_merge_enabled &&
+ headContext.lastAttemptHead &&
+ headContext.currentHead &&
+ !headContext.sameHead &&
+ !isReadyForMerge(pr),
+ );
+}
+
+export function deriveCIAutomationQueueState(
+ pr: TaskPR,
+ options: Pick,
+ state: TaskCIPRAutomationState | undefined,
+): CIAutomationQueueRecoveryState {
+ if (hasActiveQueueEntry(pr)) {
+ return {
+ context: "queued",
+ status: "queued",
+ removalCause: "unknown",
+ repairAccepted: false,
+ waitingForCommit: false,
+ };
+ }
+
+ const removalID = pr.merge_queue_last_removal_id?.trim() ?? "";
+ if (!removalID) {
+ return {
+ context: "normal",
+ status: "none",
+ removalCause: "unknown",
+ repairAccepted: false,
+ waitingForCommit: false,
+ };
+ }
+ return queueStateForRemoval(pr, options, state, removalID);
+}
+
/**
* Selects the given PR's own automation switches out of the task-scoped
* pr_options array. Falls back to all-off defaults when the PR has no
diff --git a/apps/web/lib/types/github.ts b/apps/web/lib/types/github.ts
index 0728474769..b7598e2718 100644
--- a/apps/web/lib/types/github.ts
+++ b/apps/web/lib/types/github.ts
@@ -264,12 +264,20 @@ export type TaskPR = {
closed_at: string | null;
last_synced_at: string | null;
updated_at: string;
+ /** Current pull-request head used to explain safe queue recovery. */
+ head_sha?: string;
/** Empty when GitHub did not return an active merge-queue entry. */
merge_queue_state?: MergeQueueState;
+ merge_queue_entry_id?: string;
+ merge_queue_entry_head_sha?: string;
/** GitHub's one-based queue position, when available. */
merge_queue_position?: number | null;
/** GitHub's estimated time to merge in seconds, when available. */
merge_queue_estimated_time_to_merge_seconds?: number | null;
+ merge_queue_last_removal_id?: string;
+ merge_queue_last_removed_at?: string | null;
+ merge_queue_last_removal_reason?: string;
+ merge_queue_last_removal_before_sha?: string;
// The five PR-outcome-attribution fields below are always present on a
// real API/WS payload (the backend sends every key, never omits one) — the
// `?:` here follows this file's existing convention for nullable fields
@@ -301,6 +309,14 @@ export type TaskPRDeletedEvent = {
association_id: string;
};
+export type CIAutomationQueueRemovalCause =
+ | "checks_failed"
+ | "checks_timed_out"
+ | "conflict"
+ | "manual"
+ | "branch_protection"
+ | "unknown";
+
export type TaskCIPRAutomationState = {
task_id: string;
repository_id: string;
@@ -313,6 +329,9 @@ export type TaskCIPRAutomationState = {
auto_fix_exhausted_at: string | null;
last_merge_signature: string;
last_merge_attempt_at: string | null;
+ last_queue_attempt_head_sha?: string;
+ last_queue_fix_event_id?: string;
+ last_queue_removal_cause?: CIAutomationQueueRemovalCause | string;
review_request_initialized?: boolean;
last_review_requested?: boolean;
last_observed_pr_state?: string;
diff --git a/apps/web/src/locales/en/github.json b/apps/web/src/locales/en/github.json
index 738df85c5a..f5256f6f6f 100644
--- a/apps/web/src/locales/en/github.json
+++ b/apps/web/src/locales/en/github.json
@@ -44,7 +44,7 @@
"autoFixExhaustedAria": "auto-fix paused after reaching {{current}} of {{max}} rounds",
"autoFixCiAndAddressComments": "Auto-fix CI and address comments",
"autoFixPrompt": "Auto-fix prompt",
- "autoFixPromptDescription": "This prompt is used only for this task. Leave it blank to use the default prompt. Add <0>{{placeholder}}0> when you want Kandev to include its PR feedback snapshot.",
+ "autoFixPromptDescription": "This prompt is used only for this task. Leave it blank to use the default prompt. Add <0>{{placeholder}}0> when you want Kandev to include PR feedback, queue-removal context, and available failed merge-group checks.",
"autoFixRoundExplanation": "Auto-fix has used {{current}} of {{max}} rounds for this PR. A round is counted when Kandev sends or queues a CI auto-fix message. Kandev waits for all PR checks to finish before starting a new CI auto-fix turn, so the agent gets the final failed checks and current comments together. Updating an already queued auto-fix message does not use another round. When this is at {{max}}/{{max}} and there is no pending auto-fix message left to update, Kandev pauses auto-fix for this PR so it cannot loop forever. Disable and re-enable auto-fix after manual review to start over.",
"autoMerge": "Auto-merge",
"autoMergeEnabledAria": "auto-merge enabled",
@@ -503,7 +503,7 @@
"theAppIsReadyToSelect": "The App is ready to select and install for this workspace.",
"theExistingWorkspaceConnectionWasNot": "The existing workspace connection was not changed.",
"theGithubResponseCouldNotBe": "The GitHub response could not be verified. Review the connection and try again.",
- "thePlaceholderInsertsTheCurrentPr": "The placeholder inserts the current PR identifier, new or changed failing checks with GitHub job links, and new or changed review comments with file, line, and body text.",
+ "thePlaceholderInsertsTheCurrentPr": "The placeholder inserts the current PR identifier, queue-removal context, new or changed failing checks only when an exact merge-group commit is available, and new or changed review comments.",
"thePullRequestIsStillLinked": "The pull request is still linked.",
"theRepositoryWillBeAutomaticallyCloned": "The repository will be automatically cloned to {{path}} if it is not already present in the workspace.",
"theSettingsCouldNotBeSaved": "The settings could not be saved",
@@ -588,5 +588,32 @@
"mergeQueueStateQueued": "Queued",
"mergeQueueStateUnmergeable": "Unmergeable",
"mergeQueueStatus": "Merge queue: {{state}}",
- "mergeQueueSubMinuteEstimate": "Estimated merge time: less than a minute."
+ "mergeQueueSubMinuteEstimate": "Estimated merge time: less than a minute.",
+ "autoMergeOrRequeueWhenReady": "Auto-merge or requeue when ready",
+ "autoFixQueueSupportQueued": "Will repair failed queue checks or conflicts.",
+ "autoFixQueueSupportEnable": "Enable to send this actionable queue failure to the agent.",
+ "autoFixQueueSupportRequested": "Repair requested for this removal.",
+ "autoFixQueueSupportNotActionable": "No automatic repair for this removal.",
+ "autoFixQueueSupportNewHead": "The fix created a new commit.",
+ "autoMergeQueueSupportQueued": "Current queue attempt is already active.",
+ "autoMergeQueueSupportDisabled": "Automatic requeue is off.",
+ "autoMergeQueueSupportWaitingForCommit": "Waiting for a new commit before requeue.",
+ "autoMergeQueueSupportWaitingForChecks": "Waiting for required checks.",
+ "autoMergeQueueSupportNewHead": "Will requeue after a new commit passes checks.",
+ "automationForPrQueued": "PR #{{number}} is in the merge queue",
+ "automationForPrRemoved": "PR #{{number}} was removed: {{cause}}",
+ "automationForPrRemovedGeneric": "PR #{{number}} was removed from the merge queue",
+ "ciAutomationQueueRecoveryHelp": "Auto-fix repairs actionable queue removals. Auto-merge submits an eligible head or requeues it after a new commit. Both controls form the repair and requeue loop. Kandev never requeues the same head after removal.",
+ "mergeQueueAutomation": "Merge queue automation",
+ "mergeQueueRecovery": "Merge queue recovery",
+ "mergeQueueRemovalCauseChecksFailed": "checks failed",
+ "mergeQueueRemovalCauseChecksTimedOut": "checks timed out",
+ "mergeQueueRemovalCauseConflict": "merge conflict",
+ "mergeQueueRecoveryStatusQueued": "Active merge queue attempt. Auto-merge will not submit a duplicate.",
+ "mergeQueueRecoveryStatusActionable": "Queue removal needs attention. Enable auto-fix to send it to the agent.",
+ "mergeQueueRecoveryStatusNotActionable": "Queue removal recorded. No automatic repair for this removal.",
+ "mergeQueueRecoveryStatusRepairRequested": "Repair requested for this removal.",
+ "mergeQueueRecoveryStatusRepairRequestedWaiting": "Repair requested. Waiting for a new commit before requeue.",
+ "mergeQueueRecoveryStatusWaitingForCommit": "Waiting for a new commit before requeue.",
+ "mergeQueueRecoveryStatusWaitingForChecks": "New head detected. Waiting for required checks."
}
diff --git a/apps/web/src/locales/pseudo/github.json b/apps/web/src/locales/pseudo/github.json
index ab08015bc7..37727ebba8 100644
--- a/apps/web/src/locales/pseudo/github.json
+++ b/apps/web/src/locales/pseudo/github.json
@@ -44,7 +44,7 @@
"autoFixExhaustedAria": "àũţō-ƒĩx ƥàũśēď àƒţēŕ ŕēàćĥĩńĝ {{current}} ōƒ {{max}} ŕōũńďś",
"autoFixCiAndAddressComments": "Àũţō-ƒĩx ĆĨ àńď àďďŕēśś ćōḿḿēńţś",
"autoFixPrompt": "Àũţō-ƒĩx ƥŕōḿƥţ",
- "autoFixPromptDescription": "Ţĥĩś ƥŕōḿƥţ ĩś ũśēď ōńĺŷ ƒōŕ ţĥĩś ţàśķ. Ĺēàvē ĩţ ƀĺàńķ ţō ũśē ţĥē ďēƒàũĺţ ƥŕōḿƥţ. Àďď <0>{{placeholder}}0> ŵĥēń ŷōũ ŵàńţ Ķàńďēv ţō ĩńćĺũďē ĩţś ƤŔ ƒēēďƀàćķ śńàƥśĥōţ.",
+ "autoFixPromptDescription": "Ţĥĩś ƥŕōḿƥţ ĩś ũśēď ōńĺŷ ƒōŕ ţĥĩś ţàśķ. Ĺēàvē ĩţ ƀĺàńķ ţō ũśē ţĥē ďēƒàũĺţ ƥŕōḿƥţ. Àďď <0>{{placeholder}}0> ŵĥēń ŷōũ ŵàńţ Ķàńďēv ţō ĩńćĺũďē ƤŔ ƒēēďƀàćķ, qũēũē-ŕēḿōvàĺ ćōńţēxţ, àńď àvàĩĺàƀĺē ƒàĩĺēď ḿēŕĝē-ĝŕōũƥ ćĥēćķś.",
"autoFixRoundExplanation": "Àũţō-ƒĩx ĥàś ũśēď {{current}} ōƒ {{max}} ŕōũńďś ƒōŕ ţĥĩś ƤŔ. À ŕōũńď ĩś ćōũńţēď ŵĥēń Ķàńďēv śēńďś ōŕ qũēũēś à ĆĨ àũţō-ƒĩx ḿēśśàĝē. Ķàńďēv ŵàĩţś ƒōŕ àĺĺ ƤŔ ćĥēćķś ţō ƒĩńĩśĥ ƀēƒōŕē śţàŕţĩńĝ à ńēŵ ĆĨ àũţō-ƒĩx ţũŕń, śō ţĥē àĝēńţ ĝēţś ţĥē ƒĩńàĺ ƒàĩĺēď ćĥēćķś àńď ćũŕŕēńţ ćōḿḿēńţś ţōĝēţĥēŕ. Ũƥďàţĩńĝ àń àĺŕēàďŷ qũēũēď àũţō-ƒĩx ḿēśśàĝē ďōēś ńōţ ũśē àńōţĥēŕ ŕōũńď. Ŵĥēń ţĥĩś ĩś àţ {{max}}/{{max}} àńď ţĥēŕē ĩś ńō ƥēńďĩńĝ àũţō-ƒĩx ḿēśśàĝē ĺēƒţ ţō ũƥďàţē, Ķàńďēv ƥàũśēś àũţō-ƒĩx ƒōŕ ţĥĩś ƤŔ śō ĩţ ćàńńōţ ĺōōƥ ƒōŕēvēŕ. Ďĩśàƀĺē àńď ŕē-ēńàƀĺē àũţō-ƒĩx àƒţēŕ ḿàńũàĺ ŕēvĩēŵ ţō śţàŕţ ōvēŕ.",
"autoMerge": "Àũţō-ḿēŕĝē",
"autoMergeEnabledAria": "àũţō-ḿēŕĝē ēńàƀĺēď",
@@ -503,7 +503,7 @@
"theAppIsReadyToSelect": "Ţĥē Àƥƥ ĩś ŕēàďŷ ţō śēĺēćţ àńď ĩńśţàĺĺ ƒōŕ ţĥĩś ŵōŕķśƥàćē.",
"theExistingWorkspaceConnectionWasNot": "Ţĥē ēxĩśţĩńĝ ŵōŕķśƥàćē ćōńńēćţĩōń ŵàś ńōţ ćĥàńĝēď.",
"theGithubResponseCouldNotBe": "Ţĥē ĜĩţĤũƀ ŕēśƥōńśē ćōũĺď ńōţ ƀē vēŕĩƒĩēď. Ŕēvĩēŵ ţĥē ćōńńēćţĩōń àńď ţŕŷ àĝàĩń.",
- "thePlaceholderInsertsTheCurrentPr": "Ţĥē ƥĺàćēĥōĺďēŕ ĩńśēŕţś ţĥē ćũŕŕēńţ ƤŔ ĩďēńţĩƒĩēŕ, ńēŵ ōŕ ćĥàńĝēď ƒàĩĺĩńĝ ćĥēćķś ŵĩţĥ ĜĩţĤũƀ ĵōƀ ĺĩńķś, àńď ńēŵ ōŕ ćĥàńĝēď ŕēvĩēŵ ćōḿḿēńţś ŵĩţĥ ƒĩĺē, ĺĩńē, àńď ƀōďŷ ţēxţ.",
+ "thePlaceholderInsertsTheCurrentPr": "Ţĥē ƥĺàćēĥōĺďēŕ ĩńśēŕţś ţĥē ćũŕŕēńţ ƤŔ ĩďēńţĩƒĩēŕ, qũēũē-ŕēḿōvàĺ ćōńţēxţ, ńēŵ ōŕ ćĥàńĝēď ƒàĩĺĩńĝ ćĥēćķś ōńĺŷ ŵĥēń àń ēxàćţ ḿēŕĝē-ĝŕōũƥ ćōḿḿĩţ ĩś àvàĩĺàƀĺē, àńď ńēŵ ōŕ ćĥàńĝēď ŕēvĩēŵ ćōḿḿēńţś.",
"thePullRequestIsStillLinked": "Ţĥē ƥũĺĺ ŕēqũēśţ ĩś śţĩĺĺ ĺĩńķēď.",
"theRepositoryWillBeAutomaticallyCloned": "Ţĥē ŕēƥōśĩţōŕŷ ŵĩĺĺ ƀē àũţōḿàţĩćàĺĺŷ ćĺōńēď ţō {{path}} ĩƒ ĩţ ĩś ńōţ àĺŕēàďŷ ƥŕēśēńţ ĩń ţĥē ŵōŕķśƥàćē.",
"theSettingsCouldNotBeSaved": "Ţĥē śēţţĩńĝś ćōũĺď ńōţ ƀē śàvēď",
@@ -588,5 +588,32 @@
"mergeQueueStateQueued": "Qũēũēď",
"mergeQueueStateUnmergeable": "Ũńḿēŕĝēàƀĺē",
"mergeQueueStatus": "Ḿēŕĝē qũēũē: {{state}}",
- "mergeQueueSubMinuteEstimate": "Ēśţĩḿàţēď ḿēŕĝē ţĩḿē: ĺēśś ţĥàń à ḿĩńũţē."
+ "mergeQueueSubMinuteEstimate": "Ēśţĩḿàţēď ḿēŕĝē ţĩḿē: ĺēśś ţĥàń à ḿĩńũţē.",
+ "autoMergeOrRequeueWhenReady": "Àũţō-ḿēŕĝē ōŕ ŕēqũēũē ŵĥēń ŕēàďŷ",
+ "autoFixQueueSupportQueued": "Ŵĩĺĺ ŕēƥàĩŕ ƒàĩĺēď qũēũē ćĥēćķś ōŕ ćōńƒĺĩćţś.",
+ "autoFixQueueSupportEnable": "Ēńàƀĺē ţō śēńď ţĥĩś àćţĩōńàƀĺē qũēũē ƒàĩĺũŕē ţō ţĥē àĝēńţ.",
+ "autoFixQueueSupportRequested": "Ŕēƥàĩŕ ŕēqũēśţēď ƒōŕ ţĥĩś ŕēḿōvàĺ.",
+ "autoFixQueueSupportNotActionable": "Ńō àũţōḿàţĩć ŕēƥàĩŕ ƒōŕ ţĥĩś ŕēḿōvàĺ.",
+ "autoFixQueueSupportNewHead": "Ţĥē ƒĩx ćŕēàţēď à ńēŵ ćōḿḿĩţ.",
+ "autoMergeQueueSupportQueued": "Ćũŕŕēńţ qũēũē àţţēḿƥţ ĩś àĺŕēàďŷ àćţĩvē.",
+ "autoMergeQueueSupportDisabled": "Àũţōḿàţĩć ŕēqũēũē ĩś ōƒƒ.",
+ "autoMergeQueueSupportWaitingForCommit": "Ŵàĩţĩńĝ ƒōŕ à ńēŵ ćōḿḿĩţ ƀēƒōŕē ŕēqũēũē.",
+ "autoMergeQueueSupportWaitingForChecks": "Ŵàĩţĩńĝ ƒōŕ ŕēqũĩŕēď ćĥēćķś.",
+ "autoMergeQueueSupportNewHead": "Ŵĩĺĺ ŕēqũēũē àƒţēŕ à ńēŵ ćōḿḿĩţ ƥàśśēś ćĥēćķś.",
+ "automationForPrQueued": "ƤŔ #{{number}} ĩś ĩń ţĥē ḿēŕĝē qũēũē",
+ "automationForPrRemoved": "ƤŔ #{{number}} ŵàś ŕēḿōvēď: {{cause}}",
+ "automationForPrRemovedGeneric": "ƤŔ #{{number}} ŵàś ŕēḿōvēď ƒŕōḿ ţĥē ḿēŕĝē qũēũē",
+ "ciAutomationQueueRecoveryHelp": "Àũţō-ƒĩx ŕēƥàĩŕś àćţĩōńàƀĺē qũēũē ŕēḿōvàĺś. Àũţō-ḿēŕĝē śũƀḿĩţś àń ēĺĩĝĩƀĺē ĥēàď ōŕ ŕēqũēũēś ĩţ àƒţēŕ à ńēŵ ćōḿḿĩţ. Ɓōţĥ ćōńţŕōĺś ƒōŕḿ ţĥē ŕēƥàĩŕ àńď ŕēqũēũē ĺōōƥ. Ķàńďēv ńēvēŕ ŕēqũēũēś ţĥē śàḿē ĥēàď àƒţēŕ ŕēḿōvàĺ.",
+ "mergeQueueAutomation": "Ḿēŕĝē qũēũē àũţōḿàţĩōń",
+ "mergeQueueRecovery": "Ḿēŕĝē qũēũē ŕēćōvēŕŷ",
+ "mergeQueueRemovalCauseChecksFailed": "ćĥēćķś ƒàĩĺēď",
+ "mergeQueueRemovalCauseChecksTimedOut": "ćĥēćķś ţĩḿēď ōũţ",
+ "mergeQueueRemovalCauseConflict": "ḿēŕĝē ćōńƒĺĩćţ",
+ "mergeQueueRecoveryStatusQueued": "Àćţĩvē ḿēŕĝē qũēũē àţţēḿƥţ. Àũţō-ḿēŕĝē ŵĩĺĺ ńōţ śũƀḿĩţ à ďũƥĺĩćàţē.",
+ "mergeQueueRecoveryStatusActionable": "Qũēũē ŕēḿōvàĺ ńēēďś àţţēńţĩōń. Ēńàƀĺē àũţō-ƒĩx ţō śēńď ĩţ ţō ţĥē àĝēńţ.",
+ "mergeQueueRecoveryStatusNotActionable": "Qũēũē ŕēḿōvàĺ ŕēćōŕďēď. Ńō àũţōḿàţĩć ŕēƥàĩŕ ƒōŕ ţĥĩś ŕēḿōvàĺ.",
+ "mergeQueueRecoveryStatusRepairRequested": "Ŕēƥàĩŕ ŕēqũēśţēď ƒōŕ ţĥĩś ŕēḿōvàĺ.",
+ "mergeQueueRecoveryStatusRepairRequestedWaiting": "Ŕēƥàĩŕ ŕēqũēśţēď. Ŵàĩţĩńĝ ƒōŕ à ńēŵ ćōḿḿĩţ ƀēƒōŕē ŕēqũēũē.",
+ "mergeQueueRecoveryStatusWaitingForCommit": "Ŵàĩţĩńĝ ƒōŕ à ńēŵ ćōḿḿĩţ ƀēƒōŕē ŕēqũēũē.",
+ "mergeQueueRecoveryStatusWaitingForChecks": "Ńēŵ ĥēàď ďēţēćţēď. Ŵàĩţĩńĝ ƒōŕ ŕēqũĩŕēď ćĥēćķś."
}
diff --git a/apps/web/src/locales/pt-pt/github.json b/apps/web/src/locales/pt-pt/github.json
index 12268994c4..bea3541222 100644
--- a/apps/web/src/locales/pt-pt/github.json
+++ b/apps/web/src/locales/pt-pt/github.json
@@ -42,7 +42,7 @@
"autoFixExhaustedAria": "correção automática pausada após atingir {{current}} de {{max}} rondas",
"autoFixCiAndAddressComments": "Corrigir automaticamente o CI e responder aos comentários",
"autoFixPrompt": "Prompt da correção automática",
- "autoFixPromptDescription": "Este prompt é usado apenas nesta tarefa. Deixe-o em branco para usar o prompt pré-definido. Adicione <0>{{placeholder}}0> quando quiser que o Kandev inclua o seu instantâneo do feedback do PR.",
+ "autoFixPromptDescription": "Este prompt é usado apenas nesta tarefa. Deixe-o em branco para usar o prompt pré-definido. Adicione <0>{{placeholder}}0> quando quiser que o Kandev inclua feedback do PR, contexto de remoção da fila e verificações falhadas de merge-group disponíveis.",
"autoFixRoundExplanation": "A correção automática já usou {{current}} de {{max}} rondas neste PR. Uma ronda é contada quando o Kandev envia ou coloca em fila uma mensagem de correção automática do CI. O Kandev espera que todas as verificações do PR terminem antes de iniciar um novo turno de correção automática do CI, para que o agente receba em conjunto as verificações falhadas finais e os comentários atuais. Atualizar uma mensagem de correção automática já em fila não consome outra ronda. Quando isto estiver em {{max}}/{{max}} e não restar nenhuma mensagem de correção automática pendente para atualizar, o Kandev suspende a correção automática neste PR para que não entre num ciclo infinito. Desative e reative a correção automática após revisão manual para recomeçar.",
"autoMerge": "Merge automático",
"autoMergeWhenReady": "Fazer merge automático quando estiver pronto",
@@ -484,7 +484,7 @@
"theAppIsReadyToSelect": "A App está pronta a ser selecionada e instalada nesta área de trabalho.",
"theExistingWorkspaceConnectionWasNot": "A ligação existente da área de trabalho não foi alterada.",
"theGithubResponseCouldNotBe": "Não foi possível verificar a resposta do GitHub. Reveja a ligação e tente novamente.",
- "thePlaceholderInsertsTheCurrentPr": "O marcador insere o identificador atual do PR, as verificações falhadas novas ou alteradas com ligações para os jobs do GitHub, e os comentários de revisão novos ou alterados com ficheiro, linha e texto.",
+ "thePlaceholderInsertsTheCurrentPr": "O marcador insere o identificador atual do PR, o contexto de remoção da fila, novos comentários de revisão e verificações falhadas do merge-group apenas quando existe um commit exato.",
"thePullRequestIsStillLinked": "O pull request continua associado.",
"theRepositoryWillBeAutomaticallyCloned": "O repositório será clonado automaticamente para {{path}} se ainda não estiver presente na área de trabalho.",
"theSettingsCouldNotBeSaved": "Não foi possível guardar as definições",
@@ -588,5 +588,32 @@
"mergeQueueStateQueued": "Em fila",
"mergeQueueStateUnmergeable": "Não integrável",
"mergeQueueStatus": "Fila de merge: {{state}}",
- "mergeQueueSubMinuteEstimate": "Tempo estimado até ao merge: menos de um minuto."
+ "mergeQueueSubMinuteEstimate": "Tempo estimado até ao merge: menos de um minuto.",
+ "autoMergeOrRequeueWhenReady": "Fazer merge ou voltar a colocar em fila quando estiver pronto",
+ "autoFixQueueSupportQueued": "Corrige verificações falhadas ou conflitos da fila.",
+ "autoFixQueueSupportEnable": "Ative para enviar esta falha acionável da fila ao agente.",
+ "autoFixQueueSupportRequested": "Correção pedida para esta remoção.",
+ "autoFixQueueSupportNotActionable": "Não há correção automática para esta remoção.",
+ "autoFixQueueSupportNewHead": "A correção criou um novo commit.",
+ "autoMergeQueueSupportQueued": "A tentativa atual na fila já está ativa.",
+ "autoMergeQueueSupportDisabled": "A recolocação automática em fila está desligada.",
+ "autoMergeQueueSupportWaitingForCommit": "A aguardar um novo commit antes de voltar à fila.",
+ "autoMergeQueueSupportWaitingForChecks": "A aguardar verificações obrigatórias.",
+ "autoMergeQueueSupportNewHead": "Volta à fila depois de um novo commit passar as verificações.",
+ "automationForPrQueued": "O PR #{{number}} está na fila de merge",
+ "automationForPrRemoved": "O PR #{{number}} foi removido: {{cause}}",
+ "automationForPrRemovedGeneric": "O PR #{{number}} foi removido da fila de merge",
+ "ciAutomationQueueRecoveryHelp": "A correção automática repara remoções acionáveis da fila. O merge automático envia um head elegível ou volta a colocá-lo na fila depois de um novo commit. As duas opções formam o ciclo de reparação e fila. O Kandev nunca volta a colocar o mesmo head na fila depois de uma remoção.",
+ "mergeQueueAutomation": "Automatização da fila de merge",
+ "mergeQueueRecovery": "Recuperação da fila de merge",
+ "mergeQueueRemovalCauseChecksFailed": "as verificações falharam",
+ "mergeQueueRemovalCauseChecksTimedOut": "as verificações excederam o tempo",
+ "mergeQueueRemovalCauseConflict": "conflito de merge",
+ "mergeQueueRecoveryStatusQueued": "Tentativa ativa na fila de merge. O merge automático não envia uma duplicada.",
+ "mergeQueueRecoveryStatusActionable": "A remoção da fila precisa de atenção. Ative a correção automática para a enviar ao agente.",
+ "mergeQueueRecoveryStatusNotActionable": "Remoção da fila registada. Não há correção automática para esta remoção.",
+ "mergeQueueRecoveryStatusRepairRequested": "Correção pedida para esta remoção.",
+ "mergeQueueRecoveryStatusRepairRequestedWaiting": "Correção pedida. A aguardar um novo commit antes de voltar à fila.",
+ "mergeQueueRecoveryStatusWaitingForCommit": "A aguardar um novo commit antes de voltar à fila.",
+ "mergeQueueRecoveryStatusWaitingForChecks": "Novo head detetado. A aguardar verificações obrigatórias."
}
diff --git a/apps/web/src/locales/zh-cn/github.json b/apps/web/src/locales/zh-cn/github.json
index a0d7912c87..ff1f1ccafa 100644
--- a/apps/web/src/locales/zh-cn/github.json
+++ b/apps/web/src/locales/zh-cn/github.json
@@ -44,7 +44,7 @@
"autoFixExhaustedAria": "达到 {{current}} / {{max}} 轮后,自动修复已暂停",
"autoFixCiAndAddressComments": "自动修复 CI 并处理评论",
"autoFixPrompt": "自动修复提示词",
- "autoFixPromptDescription": "此提示词仅用于本任务。留空则使用默认提示词。若希望 Kandev 附上 PR 反馈快照,请添加 <0>{{placeholder}}0>。",
+ "autoFixPromptDescription": "此提示词仅用于本任务。留空则使用默认提示词。若希望 Kandev 附上 PR 反馈、队列移除上下文和可用的合并组失败检查,请添加 <0>{{placeholder}}0>。",
"autoFixRoundExplanation": "自动修复已为该 PR 使用 {{current}} / {{max}} 轮。Kandev 每次发送或排队一条 CI 自动修复消息即计为一轮。Kandev 会等待全部 PR 检查完成后再开始新一轮自动修复,以便智能体同时获得最终的失败检查与当前评论。更新已排队的自动修复消息不会额外消耗轮次。当已达 {{max}}/{{max}} 且没有待更新的自动修复消息时,Kandev 会暂停该 PR 的自动修复,避免无限循环。人工审阅后,请关闭并重新启用自动修复以重新开始。",
"autoMerge": "自动合并",
"autoMergeEnabledAria": "自动合并已启用",
@@ -503,7 +503,7 @@
"theAppIsReadyToSelect": "该应用已可供此工作区选择和安装。",
"theExistingWorkspaceConnectionWasNot": "现有工作区连接未更改。",
"theGithubResponseCouldNotBe": "无法验证 GitHub 响应。请检查连接后重试。",
- "thePlaceholderInsertsTheCurrentPr": "该占位符会插入当前 PR 标识、新增或变更的失败检查(含 GitHub 作业链接),以及新增或变更的审阅评论(含文件、行号和正文)。",
+ "thePlaceholderInsertsTheCurrentPr": "该占位符会插入当前 PR 标识、队列移除上下文、新增或变更的审阅评论,以及仅在存在准确合并组提交时提供的失败合并组检查。",
"thePullRequestIsStillLinked": "拉取请求仍保持关联。",
"theRepositoryWillBeAutomaticallyCloned": "如果工作区中尚无此仓库,将自动克隆到 {{path}}。",
"theSettingsCouldNotBeSaved": "无法保存设置",
@@ -588,5 +588,32 @@
"mergeQueueStateQueued": "排队中",
"mergeQueueStateUnmergeable": "不可合并",
"mergeQueueStatus": "合并队列:{{state}}",
- "mergeQueueSubMinuteEstimate": "预计合并时间:少于一分钟"
+ "mergeQueueSubMinuteEstimate": "预计合并时间:少于一分钟",
+ "autoMergeOrRequeueWhenReady": "就绪时自动合并或重新排队",
+ "autoFixQueueSupportQueued": "将修复队列检查失败或冲突。",
+ "autoFixQueueSupportEnable": "启用后将此可处理的队列失败发送给智能体。",
+ "autoFixQueueSupportRequested": "已请求修复此移除事件。",
+ "autoFixQueueSupportNotActionable": "不会自动修复此移除事件。",
+ "autoFixQueueSupportNewHead": "修复创建了新的提交。",
+ "autoMergeQueueSupportQueued": "当前队列尝试已经处于活动状态。",
+ "autoMergeQueueSupportDisabled": "自动重新排队已关闭。",
+ "autoMergeQueueSupportWaitingForCommit": "等待新提交后再重新排队。",
+ "autoMergeQueueSupportWaitingForChecks": "等待必需的检查。",
+ "autoMergeQueueSupportNewHead": "新提交通过检查后将重新排队。",
+ "automationForPrQueued": "PR #{{number}} 已在合并队列中",
+ "automationForPrRemoved": "PR #{{number}} 已被移除:{{cause}}",
+ "automationForPrRemovedGeneric": "PR #{{number}} 已从合并队列中移除",
+ "ciAutomationQueueRecoveryHelp": "自动修复会处理可操作的队列移除。自动合并会提交符合条件的 head,或在新提交后重新排队。两项设置共同完成修复和重新排队流程。移除后 Kandev 不会让同一个 head 再次排队。",
+ "mergeQueueAutomation": "合并队列自动化",
+ "mergeQueueRecovery": "合并队列恢复",
+ "mergeQueueRemovalCauseChecksFailed": "检查失败",
+ "mergeQueueRemovalCauseChecksTimedOut": "检查超时",
+ "mergeQueueRemovalCauseConflict": "合并冲突",
+ "mergeQueueRecoveryStatusQueued": "合并队列尝试已活动。自动合并不会提交重复请求。",
+ "mergeQueueRecoveryStatusActionable": "队列移除需要处理。启用自动修复即可将其发送给智能体。",
+ "mergeQueueRecoveryStatusNotActionable": "已记录队列移除。不会自动修复此移除事件。",
+ "mergeQueueRecoveryStatusRepairRequested": "已请求修复此移除事件。",
+ "mergeQueueRecoveryStatusRepairRequestedWaiting": "已请求修复。等待新提交后再重新排队。",
+ "mergeQueueRecoveryStatusWaitingForCommit": "等待新提交后再重新排队。",
+ "mergeQueueRecoveryStatusWaitingForChecks": "检测到新 head。等待必需的检查。"
}
diff --git a/apps/web/src/locales/zh-hk/github.json b/apps/web/src/locales/zh-hk/github.json
index 8abd17264b..964979ee7e 100644
--- a/apps/web/src/locales/zh-hk/github.json
+++ b/apps/web/src/locales/zh-hk/github.json
@@ -44,7 +44,7 @@
"autoFixExhaustedAria": "達到 {{current}} / {{max}} 輪後,自動修復已暫停",
"autoFixCiAndAddressComments": "自動修復 CI 並處理評論",
"autoFixPrompt": "自動修復提示詞",
- "autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 反饋快照,請新增 <0>{{placeholder}}0>。",
+ "autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 反饋、佇列移除上下文和可用的合併組失敗檢查,請新增 <0>{{placeholder}}0>。",
"autoFixRoundExplanation": "自動修復已為該 PR 使用 {{current}} / {{max}} 輪。Kandev 每次傳送或排隊一條 CI 自動修復訊息即計為一輪。Kandev 會等待全部 PR 檢查完成後再開始新一輪自動修復,以便代理程式同時獲得最終的失敗檢查與目前評論。更新已排隊的自動修復訊息不會額外消耗輪次。當已達 {{max}}/{{max}} 且沒有待更新的自動修復訊息時,Kandev 會暫停該 PR 的自動修復,避免無限循環。人工審閱後,請關閉並重新啓用自動修復以重新開始。",
"autoMerge": "自動合併",
"autoMergeEnabledAria": "自動合併已啓用",
@@ -503,7 +503,7 @@
"theAppIsReadyToSelect": "該應用已可供此工作區選擇和安裝。",
"theExistingWorkspaceConnectionWasNot": "現有工作區連接未更改。",
"theGithubResponseCouldNotBe": "無法驗證 GitHub 回應。請檢查連接後重試。",
- "thePlaceholderInsertsTheCurrentPr": "該佔位符會插入目前 PR 標識、新增或變更的失敗檢查(含 GitHub 作業連結),以及新增或變更的審閱評論(含檔案、行號和正文)。",
+ "thePlaceholderInsertsTheCurrentPr": "該佔位符會插入目前 PR 標識、佇列移除上下文、新增或變更的審閱評論,以及僅在存在準確合併組提交時提供的失敗合併組檢查。",
"thePullRequestIsStillLinked": "拉取要求仍保持關聯。",
"theRepositoryWillBeAutomaticallyCloned": "如果工作區中尚無此儲存庫,將自動複製到 {{path}}。",
"theSettingsCouldNotBeSaved": "無法儲存設定",
@@ -588,5 +588,32 @@
"mergeQueueStateQueued": "排隊中",
"mergeQueueStateUnmergeable": "不可合併",
"mergeQueueStatus": "合併佇列:{{state}}",
- "mergeQueueSubMinuteEstimate": "預計合併時間:少於一分鐘"
+ "mergeQueueSubMinuteEstimate": "預計合併時間:少於一分鐘",
+ "autoMergeOrRequeueWhenReady": "就緒時自動合併或重新排隊",
+ "autoFixQueueSupportQueued": "將修復佇列檢查失敗或衝突。",
+ "autoFixQueueSupportEnable": "啓用後將此可處理的佇列失敗傳送給代理程式。",
+ "autoFixQueueSupportRequested": "已請求修復此移除事件。",
+ "autoFixQueueSupportNotActionable": "不會自動修復此移除事件。",
+ "autoFixQueueSupportNewHead": "修復建立了新的提交。",
+ "autoMergeQueueSupportQueued": "目前佇列嘗試已經處於活動狀態。",
+ "autoMergeQueueSupportDisabled": "自動重新排隊已關閉。",
+ "autoMergeQueueSupportWaitingForCommit": "等待新提交後再重新排隊。",
+ "autoMergeQueueSupportWaitingForChecks": "等待必需的檢查。",
+ "autoMergeQueueSupportNewHead": "新提交通過檢查後將重新排隊。",
+ "automationForPrQueued": "PR #{{number}} 已在合併佇列中",
+ "automationForPrRemoved": "PR #{{number}} 已被移除:{{cause}}",
+ "automationForPrRemovedGeneric": "PR #{{number}} 已從合併佇列中移除",
+ "ciAutomationQueueRecoveryHelp": "自動修復會處理可操作的佇列移除。自動合併會提交符合條件的 head,或在新提交後重新排隊。兩項設定共同完成修復和重新排隊流程。移除後 Kandev 不會讓同一個 head 再次排隊。",
+ "mergeQueueAutomation": "合併佇列自動化",
+ "mergeQueueRecovery": "合併佇列恢復",
+ "mergeQueueRemovalCauseChecksFailed": "檢查失敗",
+ "mergeQueueRemovalCauseChecksTimedOut": "檢查超時",
+ "mergeQueueRemovalCauseConflict": "合併衝突",
+ "mergeQueueRecoveryStatusQueued": "合併佇列嘗試已活動。自動合併不會提交重複請求。",
+ "mergeQueueRecoveryStatusActionable": "佇列移除需要處理。啓用自動修復即可將其傳送給代理程式。",
+ "mergeQueueRecoveryStatusNotActionable": "已記錄佇列移除。不會自動修復此移除事件。",
+ "mergeQueueRecoveryStatusRepairRequested": "已請求修復此移除事件。",
+ "mergeQueueRecoveryStatusRepairRequestedWaiting": "已請求修復。等待新提交後再重新排隊。",
+ "mergeQueueRecoveryStatusWaitingForCommit": "等待新提交後再重新排隊。",
+ "mergeQueueRecoveryStatusWaitingForChecks": "檢測到新 head。等待必需的檢查。"
}
diff --git a/apps/web/src/locales/zh-tw/github.json b/apps/web/src/locales/zh-tw/github.json
index a5cd3d8165..a5de1b1ac3 100644
--- a/apps/web/src/locales/zh-tw/github.json
+++ b/apps/web/src/locales/zh-tw/github.json
@@ -44,7 +44,7 @@
"autoFixExhaustedAria": "達到 {{current}} / {{max}} 輪後,自動修復已暫停",
"autoFixCiAndAddressComments": "自動修復 CI 並處理評論",
"autoFixPrompt": "自動修復提示詞",
- "autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 意見回饋快照,請新增 <0>{{placeholder}}0>。",
+ "autoFixPromptDescription": "此提示詞僅用於本任務。留空則使用預設提示詞。若希望 Kandev 附上 PR 意見回饋、佇列移除上下文和可用的合併組失敗檢查,請新增 <0>{{placeholder}}0>。",
"autoFixRoundExplanation": "自動修復已為該 PR 使用 {{current}} / {{max}} 輪。Kandev 每次傳送或排隊一條 CI 自動修復訊息即計為一輪。Kandev 會等待全部 PR 檢查完成後再開始新一輪自動修復,以便代理程式同時獲得最終的失敗檢查與目前評論。更新已排隊的自動修復訊息不會額外消耗輪次。當已達 {{max}}/{{max}} 且沒有待更新的自動修復訊息時,Kandev 會暫停該 PR 的自動修復,避免無限迴圈。人工審閱後,請關閉並重新啟用自動修復以重新開始。",
"autoMerge": "自動合併",
"autoMergeEnabledAria": "自動合併已啟用",
@@ -503,7 +503,7 @@
"theAppIsReadyToSelect": "該應用已可供此工作區選擇和安裝。",
"theExistingWorkspaceConnectionWasNot": "現有工作區連線未更改。",
"theGithubResponseCouldNotBe": "無法驗證 GitHub 回應。請檢查連線後重試。",
- "thePlaceholderInsertsTheCurrentPr": "該佔位符會插入目前 PR 標識、新增或變更的失敗檢查(含 GitHub 作業連結),以及新增或變更的審閱評論(含檔案、行號和正文)。",
+ "thePlaceholderInsertsTheCurrentPr": "該佔位符會插入目前 PR 標識、佇列移除上下文、新增或變更的審閱評論,以及僅在存在準確合併組提交時提供的失敗合併組檢查。",
"thePullRequestIsStillLinked": "提取要求仍保持關聯。",
"theRepositoryWillBeAutomaticallyCloned": "如果工作區中尚無此儲存庫,將自動複製到 {{path}}。",
"theSettingsCouldNotBeSaved": "無法儲存設定",
@@ -588,5 +588,32 @@
"mergeQueueStateQueued": "排隊中",
"mergeQueueStateUnmergeable": "不可合併",
"mergeQueueStatus": "合併佇列:{{state}}",
- "mergeQueueSubMinuteEstimate": "預計合併時間:少於一分鐘"
+ "mergeQueueSubMinuteEstimate": "預計合併時間:少於一分鐘",
+ "autoMergeOrRequeueWhenReady": "就緒時自動合併或重新排隊",
+ "autoFixQueueSupportQueued": "將修復佇列檢查失敗或衝突。",
+ "autoFixQueueSupportEnable": "啟用後將此可處理的佇列失敗傳送給代理程式。",
+ "autoFixQueueSupportRequested": "已請求修復此移除事件。",
+ "autoFixQueueSupportNotActionable": "不會自動修復此移除事件。",
+ "autoFixQueueSupportNewHead": "修復建立了新的提交。",
+ "autoMergeQueueSupportQueued": "目前佇列嘗試已經處於活動狀態。",
+ "autoMergeQueueSupportDisabled": "自動重新排隊已關閉。",
+ "autoMergeQueueSupportWaitingForCommit": "等待新提交後再重新排隊。",
+ "autoMergeQueueSupportWaitingForChecks": "等待必需的檢查。",
+ "autoMergeQueueSupportNewHead": "新提交通過檢查後將重新排隊。",
+ "automationForPrQueued": "PR #{{number}} 已在合併佇列中",
+ "automationForPrRemoved": "PR #{{number}} 已被移除:{{cause}}",
+ "automationForPrRemovedGeneric": "PR #{{number}} 已從合併佇列中移除",
+ "ciAutomationQueueRecoveryHelp": "自動修復會處理可操作的佇列移除。自動合併會提交符合條件的 head,或在新提交後重新排隊。兩項設定共同完成修復和重新排隊流程。移除後 Kandev 不會讓同一個 head 再次排隊。",
+ "mergeQueueAutomation": "合併佇列自動化",
+ "mergeQueueRecovery": "合併佇列恢復",
+ "mergeQueueRemovalCauseChecksFailed": "檢查失敗",
+ "mergeQueueRemovalCauseChecksTimedOut": "檢查超時",
+ "mergeQueueRemovalCauseConflict": "合併衝突",
+ "mergeQueueRecoveryStatusQueued": "合併佇列嘗試已活動。自動合併不會提交重複請求。",
+ "mergeQueueRecoveryStatusActionable": "佇列移除需要處理。啟用自動修復即可將其傳送給代理程式。",
+ "mergeQueueRecoveryStatusNotActionable": "已記錄佇列移除。不會自動修復此移除事件。",
+ "mergeQueueRecoveryStatusRepairRequested": "已請求修復此移除事件。",
+ "mergeQueueRecoveryStatusRepairRequestedWaiting": "已請求修復。等待新提交後再重新排隊。",
+ "mergeQueueRecoveryStatusWaitingForCommit": "等待新提交後再重新排隊。",
+ "mergeQueueRecoveryStatusWaitingForChecks": "檢測到新 head。等待必需的檢查。"
}
diff --git a/docs/plans/github-pr-merge-queue-recovery/plan.md b/docs/plans/github-pr-merge-queue-recovery/plan.md
new file mode 100644
index 0000000000..cfecdd38d3
--- /dev/null
+++ b/docs/plans/github-pr-merge-queue-recovery/plan.md
@@ -0,0 +1,165 @@
+---
+created: 2026-08-24
+status: done
+requirements:
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003
+ - REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001
+system_design:
+ - ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
+ - ../../specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
+legacy_specs: []
+---
+# Implementation Plan: GitHub PR Merge Queue Recovery
+
+## Overview
+
+Extend the existing GitHub poller with durable queue-removal evidence. Then use
+that evidence in the current auto-fix and auto-merge evaluator.
+
+Provider state lands first because retry safety depends on a stable event ID
+and head SHA. Runtime automation follows that contract. The shared desktop and
+mobile controls then expose the result. E2E work completes the package.
+
+## Scope
+
+### In scope
+
+- Poll GitHub for the latest merge-queue removal event.
+- Persist the pull-request head, active queue-attempt identity, and removal
+ evidence.
+- Send one auto-fix round for each actionable removal.
+- Prevent same-head requeue loops.
+- Requeue a new eligible head through the existing queue-aware merge action.
+- Explain recovery in the current PR hover popover and phone drawer.
+- Update the public GitHub integration guide.
+
+### Out of scope
+
+- Merge-group webhooks.
+- Automatic flaky-check retries without a new pull-request head.
+- A third queue-recovery switch.
+- Manual queue removal or priority controls.
+- Coordinated requeue ordering for a GitHub pull-request stack.
+- GitLab merge requests.
+
+## Technical approach
+
+### Provider snapshot and persistence
+
+Extend `prFieldsBlock` and `batchedPRResult` in
+`apps/backend/internal/github/graphql.go`. Read the active queue entry, current
+head SHA, and latest `RemovedFromMergeQueueEvent`.
+
+Extend `PRStatus` and `TaskPR` in `apps/backend/internal/github/models.go`.
+Add the new SQLite columns and thread them through every TaskPR write path in
+`apps/backend/internal/github/store.go`.
+
+Extend `prepareTaskPRSyncState` in
+`apps/backend/internal/github/service_pr_watch.go`. Active queue fields clear
+on an authoritative null. The last removal remains until a newer event arrives.
+
+### Auto-fix and auto-merge
+
+Extend the CI checkpoint in
+`apps/backend/internal/orchestrator/event_handlers_github_ci_automation.go`.
+Queue-removal evidence becomes actionable only with failed-check or conflict
+evidence.
+
+Map reviewed GitHub removal-reason forms to a normalized cause. Do not treat
+the removal event's `beforeCommit` as a merge-group commit. Reuse the current
+prompt queue, coalescing key, session binding, and 10-round limit.
+
+Add the head SHA to `ciAutomationMergeSignature`. Store the last queue-attempt
+head for Kandev-created and externally observed attempts. A queue removal
+blocks the same head. A new eligible head permits one new queue-aware merge
+request.
+
+### Responsive controls and copy
+
+Keep two switches in `pr-ci-automation-rows.tsx`. Change the auto-merge label
+to `Auto-merge or requeue when ready`.
+
+Adapt the section title, subtitle, and supporting text for normal, queued, and
+removed contexts. Keep both switch labels stable. Add one compact,
+non-interactive queue status below the switches. Derive all presentation state
+in a pure helper from `TaskPR`, per-PR options, and
+`TaskCIPRAutomationState`.
+
+Use the normalized removal cause for localized copy. Use generic removal copy
+for manual, branch-protection, and unknown causes. Do not use the raw provider
+reason as a label.
+
+Update the information control and prompt dialog copy. Add every new key to all
+five required locale catalogs. Update the GitHub section in
+`docs/public/integrations.md`.
+
+### Integrated test support
+
+Extend the mock GitHub controller and E2E API client with queue-removal and head
+transition inputs. Seed an actionable removal, observe one prompt, publish a
+new head, and observe one queued merge attempt.
+
+## Tests
+
+- `AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.*`: GraphQL conversion,
+ SQLite round-trip, sync transitions, and restart-safe reads in
+ `apps/backend/internal/github`.
+- `AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.*`: evaluator tests for
+ reviewed reason forms, conflicts, deduplication, unknown removal, durable
+ dispatch, commit-identity safety, and the round limit.
+- `AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.*`: evaluator tests for
+ same-head blocking, new-head requeue, enable-while-queued adoption, and
+ GitHub rejection.
+- `AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.*`: pure state tests and focused
+ component tests for labels, help, status, and errors.
+
+## E2E tests
+
+- Desktop: add a merge-queue recovery scenario to
+ `apps/web/e2e/tests/pr/ci-automation-options.spec.ts`. Open the hover popover,
+ start with an already queued PR, enable both switches, remove the PR, and
+ observe repair, wait, and requeue states.
+- Mobile: add the same user outcome to
+ `apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts`. Use touch input
+ and the `mobile-chrome` project.
+- Both scenarios assert no duplicate request when the options are enabled on
+ the active queue entry. They then assert one repair prompt and one queue
+ request per eligible transition. The mobile scenario also asserts 44-pixel
+ rows, internal drawer scrolling, and no document-level horizontal overflow.
+
+## Work orders
+
+- [x] [Task 01: Persist queue recovery evidence](task-01-persist-queue-recovery-evidence.md)
+- [x] [Task 02: Evaluate repair and requeue](task-02-evaluate-repair-and-requeue.md)
+- [x] [Task 03: Expose responsive recovery controls](task-03-expose-responsive-recovery-controls.md)
+- [x] [Task 04: Prove desktop and mobile recovery](task-04-prove-desktop-and-mobile-recovery.md)
+
+## Verification results
+
+- `go test -tags fts5 ./internal/github ./internal/orchestrator`: 3,770 tests passed.
+- `go test -race -tags fts5 ./internal/github`: 1,661 tests passed.
+- `make -C apps/backend lint`: passed with zero issues.
+- `python3 scripts/lint-spec-files.py --all`: passed.
+- Web focused recovery and regression tests: 4 files, 69 tests passed.
+- `pnpm run typecheck`: passed.
+- Full web lint with `--max-warnings 0`: passed.
+- `pnpm run i18n:check`: passed for all five catalogs; existing orphan warnings only.
+- Public documentation tests: 61 passed; 41 pages validated.
+- Fresh-build desktop recovery E2E: 1 test passed.
+- Mobile recovery E2E: 1 test passed.
+- `git diff --check`: passed.
+
+## Risks
+
+- GitHub exposes the removal reason as free-form text. Automatic action cannot
+ use an unrecognized form. Unknown forms remain visible but do not start a
+ repair.
+- GitHub does not document the removal event's `beforeCommit` as the temporary
+ merge-group commit. The first version does not attach merge-group check logs
+ unless an exact commit identity is available.
+- A one-minute poll can first observe removal after a later push. The safe
+ fallback can delay one automatic requeue instead of risking a retry loop.
+- GitHub can remove several stacked pull requests after one lower failure. The
+ first version evaluates each linked pull request independently.
diff --git a/docs/plans/github-pr-merge-queue-recovery/task-01-persist-queue-recovery-evidence.md b/docs/plans/github-pr-merge-queue-recovery/task-01-persist-queue-recovery-evidence.md
new file mode 100644
index 0000000000..bd85ed44cd
--- /dev/null
+++ b/docs/plans/github-pr-merge-queue-recovery/task-01-persist-queue-recovery-evidence.md
@@ -0,0 +1,91 @@
+---
+id: "01-persist-queue-recovery-evidence"
+title: "Persist queue recovery evidence"
+status: done
+wave: 1
+depends_on: []
+plan: "plan.md"
+requirements:
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001
+acceptance_criteria:
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.1
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.2
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.3
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.4
+system_design:
+ - ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
+---
+# Task 01: Persist Queue Recovery Evidence
+
+## Summary
+
+Extend the GitHub PR snapshot with a stable head SHA, active queue identity,
+and the latest removal event. Persist complete observations across every
+TaskPR write path.
+
+## In scope
+
+- Extend the batched GraphQL query and conversion.
+- Add populated guards for queue entry and removal history.
+- Add the TaskPR and SQLite fields from the system design.
+- Preserve fields across create, replace, restore, and sync paths.
+- Add focused GraphQL, service, and real-store tests.
+
+## Out of scope
+
+- Auto-fix classification or dispatch.
+- Auto-merge and requeue decisions.
+- Frontend rendering.
+
+## Acceptance
+
+- A poll can detect a removal after the active entry disappears.
+- Queue-unaware reads preserve the complete recovery snapshot.
+- The snapshot survives every TaskPR write path and a backend restart.
+
+## Verification
+
+```bash
+go test -tags fts5 ./internal/github -run 'Test(PRFieldsBlockRequestsMergeQueueRecovery|ConvertBatchedPRResultPreservesMergeQueueRecovery|SyncTaskPR.*MergeQueueRecovery|TaskPRMergeQueueRecoverySchema)'
+```
+
+Run the command from `apps/backend`.
+
+## Files likely touched
+
+- `apps/backend/internal/github/graphql.go`
+- `apps/backend/internal/github/models.go`
+- `apps/backend/internal/github/store.go`
+- `apps/backend/internal/github/service_pr_watch.go`
+- `apps/backend/internal/github/graphql_merge_queue_recovery_test.go`
+- `apps/backend/internal/github/service_pr_merge_queue_recovery_test.go`
+- `apps/backend/internal/github/store_merge_queue_recovery_test.go`
+
+## Dependencies
+
+None.
+
+## Risks
+
+- A missing populated guard can erase a valid timeline observation during a
+ REST feedback refresh.
+- Schema column lists have several create, replace, and restore paths.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- Integration requirements 001 and system-design provider sections.
+- Existing merge-queue status persistence and outcome-attribution patterns.
+
+## Results
+
+- Added GraphQL head, active queue-entry, and removal-event observations with
+ populated guards.
+- Persisted and restored the recovery snapshot through TaskPR create, replace,
+ update, restore, and sync paths, including stale-event protection.
+- Added GraphQL, service, and SQLite regression coverage.
+- Verification: `go test -tags fts5 ./internal/github` passed (1,660 tests).
+- Verification: `go test -race -tags fts5 ./internal/github` passed (1,660 tests).
diff --git a/docs/plans/github-pr-merge-queue-recovery/task-02-evaluate-repair-and-requeue.md b/docs/plans/github-pr-merge-queue-recovery/task-02-evaluate-repair-and-requeue.md
new file mode 100644
index 0000000000..952c4e7311
--- /dev/null
+++ b/docs/plans/github-pr-merge-queue-recovery/task-02-evaluate-repair-and-requeue.md
@@ -0,0 +1,118 @@
+---
+id: "02-evaluate-repair-and-requeue"
+title: "Evaluate repair and requeue"
+status: done
+wave: 2
+depends_on:
+ - "01-persist-queue-recovery-evidence"
+plan: "plan.md"
+requirements:
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003
+acceptance_criteria:
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.1
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.2
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.3
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.4
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.5
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.6
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.7
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.8
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.1
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.2
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.3
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.4
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.5
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.6
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.7
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.8
+system_design:
+ - ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
+---
+# Task 02: Evaluate Repair and Requeue
+
+## Summary
+
+Extend CI automation with queue-removal checkpoints and failed-check evidence.
+Use the PR head SHA to permit one safe queue attempt after a repair.
+
+## In scope
+
+- Map reviewed removal-reason forms to normalized causes.
+- Keep `beforeCommit` separate from an exact merge-group commit identity.
+- Add queue-removal evidence to the auto-fix checkpoint and prompt snapshot.
+- Record one accepted auto-fix round per actionable removal event.
+- Add the head SHA to the merge signature.
+- Store the last accepted or observed queue-attempt head.
+- Baseline Kandev-created and externally observed queue attempts.
+- Adopt an active attempt when auto-merge is enabled while already queued.
+- Evaluate one retained actionable removal when auto-fix becomes enabled.
+- Prevent a same-head automatic requeue.
+- Add focused service and orchestrator tests.
+
+## Out of scope
+
+- A new queue preference.
+- A same-head flaky-check retry policy.
+- UI and E2E changes.
+
+## Acceptance
+
+- One actionable removal produces at most one auto-fix round.
+- A manual or unknown removal without evidence produces no repair prompt.
+- The removal event commit does not produce merge-group check evidence.
+- A new eligible head produces one queue-aware merge request.
+- Enabling auto-merge on an active entry produces no duplicate request.
+
+## Verification
+
+```bash
+go test -tags fts5 ./internal/github ./internal/orchestrator -run 'Test.*CIAutomation.*MergeQueueRecovery'
+```
+
+Run the command from `apps/backend`.
+
+## Files likely touched
+
+- `apps/backend/internal/github/service_ci_automation.go`
+- `apps/backend/internal/github/store.go`
+- `apps/backend/internal/orchestrator/event_handlers_github_ci_automation.go`
+- `apps/backend/internal/orchestrator/event_handlers_github_ci_automation_test.go`
+- `apps/backend/internal/github/service_ci_automation_test.go`
+
+## Dependencies
+
+- Task 01 supplies the durable provider snapshot.
+
+## Risks
+
+- GitHub can change its free-form removal reasons. Unknown forms must fail
+ closed and remain observable.
+- Prompt text must preserve the existing untrusted-data boundary.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- Integration requirements 002 and 003.
+- Existing CI checkpoint, queue dispatch, round-limit, and merge-signature code.
+
+## Results
+
+- Added reviewed queue-removal cause classification with fail-closed handling
+ for manual, branch-protection, and unknown provider reasons. Merge conflict
+ and `UNMERGEABLE` evidence remain actionable without treating `beforeCommit`
+ as a merge-group check identity.
+- Added queue-removal checkpoint and prompt evidence, durable event deduplication,
+ round-limit accounting, and current-head eligibility checks.
+- Added durable active-queue adoption and attempted-head persistence. The merge
+ signature now includes `TaskPR.HeadSHA`; active entries are adopted, removed
+ heads are blocked, and a changed eligible head can be queued once.
+- Added focused store, service wiring, and orchestrator coverage for the
+ recovery state machine.
+- `go test -tags fts5 ./internal/github ./internal/orchestrator -run 'Test.*CIAutomation.*MergeQueueRecovery'` passed (13 tests).
+- `go test -tags fts5 ./internal/github` passed (1,661 tests).
+- `go test -tags fts5 ./internal/orchestrator` passed.
+- `git diff --check` and targeted `gofmt -l` checks passed.
diff --git a/docs/plans/github-pr-merge-queue-recovery/task-03-expose-responsive-recovery-controls.md b/docs/plans/github-pr-merge-queue-recovery/task-03-expose-responsive-recovery-controls.md
new file mode 100644
index 0000000000..1aab956f2b
--- /dev/null
+++ b/docs/plans/github-pr-merge-queue-recovery/task-03-expose-responsive-recovery-controls.md
@@ -0,0 +1,136 @@
+---
+id: "03-expose-responsive-recovery-controls"
+title: "Expose responsive recovery controls"
+status: done
+wave: 3
+depends_on:
+ - "02-evaluate-repair-and-requeue"
+plan: "plan.md"
+requirements:
+ - REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001
+acceptance_criteria:
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.1
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.2
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.3
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.4
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.5
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.6
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.7
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.8
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.9
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.10
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.11
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.12
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.13
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.14
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.15
+system_design:
+ - ../../specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
+ - ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
+---
+# Task 03: Expose Responsive Recovery Controls
+
+## Summary
+
+Keep the two existing automation switches and add queue-aware copy and status.
+Use the same state derivation in the desktop hover popover and mobile drawer.
+
+## In scope
+
+- Add web types for queue-recovery state.
+- Update the auto-merge label.
+- Adapt the title, subtitle, and supporting text to queue context.
+- Use the normalized removal cause and generic unknown-cause fallback.
+- Add the compact queue-recovery status line.
+- Update the header information and prompt dialog copy.
+- Add focused helper and component tests.
+- Update all five locale catalogs.
+- Update `docs/public/integrations.md`.
+
+## Out of scope
+
+- New navigation or overlay surfaces.
+- A manual requeue button.
+- GitLab copy changes.
+
+## Acceptance
+
+- The surface contains two switches and no queue-recovery switch.
+- The queue status and same-head guard are clear on desktop and mobile.
+- An already queued PR explains option adoption without implying a new enqueue.
+- A removed PR explains the next repair or requeue step.
+- Raw provider text does not become a localized label or accessible name.
+- All copy is localized, accessible, and safe at narrow widths.
+
+## Verification
+
+```bash
+# Run from apps/web.
+pnpm test -- components/github/pr-ci-popover.automation.test.tsx components/github/pr-ci-automation-rows.test.tsx
+pnpm run i18n:check
+
+# Run from the repository root. The test harness resolves public-docs paths
+# relative to the current working directory.
+node --test scripts/validate-public-docs.test.mjs
+node scripts/validate-public-docs.mjs
+```
+
+Install workspace dependencies from `apps` before the first pnpm command in a
+fresh worktree.
+
+## Files likely touched
+
+- `apps/web/lib/types/github.ts`
+- `apps/web/lib/github/ci-automation.ts`
+- `apps/web/lib/github/ci-automation.test.ts`
+- `apps/web/components/github/pr-ci-automation-controls.tsx`
+- `apps/web/components/github/pr-ci-automation-rows.tsx`
+- `apps/web/components/github/pr-ci-automation-rows.test.tsx`
+- `apps/web/components/github/pr-ci-automation-prompt-dialog.tsx`
+- `apps/web/components/github/pr-ci-popover.automation.test.tsx`
+- `apps/web/src/locales/*/github.json`
+- `docs/public/integrations.md`
+
+## Dependencies
+
+- Task 02 supplies the final API state and retry semantics.
+
+## Risks
+
+- The existing compact popover has limited vertical space.
+- The label change also affects selectors and public documentation.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- UI requirement and system design.
+- Existing PR automation rows and `PRStatusChipDrawer` mobile composition.
+
+## Results
+
+Implemented the queue-aware desktop hover-popover and mobile drawer state
+surface while retaining exactly two automation switches. The pure helper now
+derives active-queue, actionable-removal, non-actionable-removal,
+repair-requested, same-head wait, and new-head wait states from durable PR and
+automation state. Titles, subtitles, supporting text, status, help, and prompt
+copy use localized keys; unknown, manual, and branch-protection causes remain
+generic and raw provider text is not exposed as a label or accessible name.
+
+Added focused helper and component coverage for queue context, recovery copy,
+same-head waiting, accessibility, and the two-switch invariant. Updated all
+five locale catalogs and `docs/public/integrations.md`.
+
+Verification:
+
+- `pnpm test -- components/github/pr-ci-popover.automation.test.tsx components/github/pr-ci-automation-rows.test.tsx components/github/pr-status-chip.test.tsx lib/github/ci-automation.test.ts`: 4 files, 69 tests passed.
+- `pnpm run typecheck`: passed.
+- `pnpm exec eslint` on the changed UI/helper files: passed with zero warnings.
+- `pnpm run i18n:check`: passed; all five required catalogs complete, pseudo
+ locale synchronized, and no untranslated non-JSX copy or em dashes.
+- `node --test scripts/validate-public-docs.test.mjs` from the repository root:
+ 61 tests passed.
+- `node scripts/validate-public-docs.mjs` from the repository root: 41 pages
+ validated.
diff --git a/docs/plans/github-pr-merge-queue-recovery/task-04-prove-desktop-and-mobile-recovery.md b/docs/plans/github-pr-merge-queue-recovery/task-04-prove-desktop-and-mobile-recovery.md
new file mode 100644
index 0000000000..7d92e9b31d
--- /dev/null
+++ b/docs/plans/github-pr-merge-queue-recovery/task-04-prove-desktop-and-mobile-recovery.md
@@ -0,0 +1,117 @@
+---
+id: "04-prove-desktop-and-mobile-recovery"
+title: "Prove desktop and mobile recovery"
+status: done
+wave: 4
+depends_on:
+ - "03-expose-responsive-recovery-controls"
+plan: "plan.md"
+requirements:
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003
+ - REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001
+acceptance_criteria:
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.1
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.3
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.2
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.3
+ - AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.7
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.8
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.9
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.10
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.11
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.12
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.13
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.14
+ - AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.15
+system_design:
+ - ../../specs/integrations/system-design/github-pr-merge-queue-recovery.md
+ - ../../specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
+---
+# Task 04: Prove Desktop and Mobile Recovery
+
+## Summary
+
+Add deterministic mock GitHub transitions for queue removal and a new head.
+Prove the same repair-and-requeue outcome through desktop hover and mobile tap
+surfaces.
+
+## In scope
+
+- Extend mock GitHub seeding for queue entry, removal, head, and check evidence.
+- Add API-client helpers for the E2E transitions.
+- Add a desktop hover-popover recovery scenario.
+- Add a mobile drawer recovery scenario.
+- Start both scenarios with the pull request already in the queue.
+- Assert contextual title, subtitle, supporting text, and status transitions.
+- Assert the localized classified cause and generic unknown-cause fallback.
+- Prove deduplication, touch geometry, scroll ownership, and overflow behavior.
+
+## Out of scope
+
+- Live writes to GitHub.
+- Product-video or screenshot capture.
+- Full merge-queue management.
+
+## Acceptance
+
+- One removal produces one visible repair round on desktop and mobile.
+- Enabling both options on an active entry produces no second queue request.
+- The same head does not requeue. A new eligible head queues once.
+- The mobile drawer keeps 44-pixel rows and no document overflow.
+
+## Verification
+
+```bash
+pnpm e2e:run tests/pr/ci-automation-options.spec.ts -- --grep "merge queue recovery"
+pnpm e2e:run --no-build --project mobile-chrome tests/pr/mobile-ci-automation-options.spec.ts -- --grep "merge queue recovery"
+```
+
+Run the commands from `apps/web`. Run the first command with a fresh build.
+
+## Files likely touched
+
+- `apps/backend/internal/github/mock_client.go`
+- `apps/backend/internal/github/mock_controller.go`
+- `apps/web/e2e/helpers/api-client.ts`
+- `apps/web/e2e/tests/pr/ci-automation-options.spec.ts`
+- `apps/web/e2e/tests/pr/mobile-ci-automation-options.spec.ts`
+
+## Dependencies
+
+- Task 03 completes the integrated backend and UI behavior.
+
+## Risks
+
+- The mock must reproduce the provider event order without fixed sleeps.
+- The desktop test must reset pointer state before hover assertions.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- Both system designs.
+- Existing CI automation and merge-queue E2E fixtures.
+
+## Results
+
+Added deterministic mock GitHub merge-queue transitions and merge-attempt
+inspection. The mock provider now carries queue membership, removal evidence,
+head changes, check runs, and queued merge outcomes through the normal status
+sync and event bus.
+
+Added desktop hover-popover and mobile drawer scenarios. Both start with an
+active queue entry, enable both existing switches without a duplicate merge
+request, observe one classified actionable repair round, block a same-head
+requeue, queue exactly one new eligible head, and observe the resulting active
+queue entry. The mobile scenario also uses touch input and verifies 44-pixel
+switch rows, drawer-owned scrolling, and no document-level horizontal
+overflow. GitHub selectors and component expectations were updated for the
+new auto-merge/requeue label.
+
+Verification:
+
+- `pnpm e2e:run tests/pr/ci-automation-options.spec.ts -- --grep "merge queue recovery"`: fresh-build desktop run passed (1 test).
+- `pnpm e2e:run --no-build --project mobile-chrome tests/pr/mobile-ci-automation-options.spec.ts -- --grep "merge queue recovery"`: mobile run passed (1 test).
diff --git a/docs/public/integrations.md b/docs/public/integrations.md
index 7d4d07e2be..cfaf4b6fb4 100644
--- a/docs/public/integrations.md
+++ b/docs/public/integrations.md
@@ -312,12 +312,14 @@ Repository scope, authentication, and watch filters are workspace-specific. Repo
### Automate a linked pull request
-For a task with linked GitHub pull requests, open the PR status control above the task chat input. The automation controls, **Auto-fix CI & address comments**, **Auto-merge when ready**, **Your review is requested**, **PR merged**, and **PR closed without merging**, are scoped to whichever linked PR's tab is selected. Enabling a control for one linked PR does not enable it for the task's other linked PRs; Kandev tracks delivery and deduplication separately for each linked PR. The saved auto-fix prompt override applies to every linked PR.
+For a task with linked GitHub pull requests, open the PR status control above the task chat input. The automation controls, **Auto-fix CI & address comments**, **Auto-merge or requeue when ready**, **Your review is requested**, **PR merged**, and **PR closed without merging**, are scoped to whichever linked PR's tab is selected. Enabling a control for one linked PR does not enable it for the task's other linked PRs; Kandev tracks delivery and deduplication separately for each linked PR. The saved auto-fix prompt override applies to every linked PR.
This is a GitHub-only lifecycle feature. Kandev reuses the existing lightweight task PR poller, which checks watched linked PRs roughly once per minute; it does not add a separate scheduler. Saving enabled options also evaluates the task's current linked PRs without waiting for the next poll.
When GitHub puts a linked pull request in a merge queue, the PR status control shows its queue state. It also shows the queue position and estimated merge time when GitHub provides them. The same status appears in the task summary, the mobile PR chip, and Review.
+The existing two automation switches also control merge-queue recovery. Auto-fix sends one actionable queue removal to the linked task agent and counts it as one auto-fix round. Auto-merge submits an eligible pull request through GitHub's queue-aware merge action. If GitHub removes a queue attempt, Kandev records the reason and waits for a new pull-request head before it submits another attempt; it never retries the same head automatically. An active queue entry is adopted when auto-merge is enabled, so enabling the option does not submit a duplicate request. Unknown, manual, and branch-protection removals remain visible but do not start automatic repair.
+
**Your review is requested** matches the GitHub account connected to the task's workspace. The first observation is a quiet baseline. Any later transition to a request for that account wakes the agent, including the first new request after baselining and a re-review request after changes. Clearing a request rearms the next transition. If the workspace's connected GitHub account changes, Kandev quietly rebinds the task and re-establishes every linked PR's baseline; switching accounts does not itself create a prompt.
**PR merged** and **PR closed without merging** are separate subscriptions to the same kind of follow-up: waking the agent when review work ends. Each notifies once when its linked PR enters the selected terminal state. Kandev delivers lifecycle notifications to the task's active promptable session, preferring the primary session. It does not interrupt a busy session: it queues a message for delivery when that session is available. If the task has no promptable session, Kandev records the per-PR delivery error, creates no new session, and retries once a session becomes promptable.
diff --git a/docs/specs/integrations/README.md b/docs/specs/integrations/README.md
index a91a66be80..d7f47e01b5 100644
--- a/docs/specs/integrations/README.md
+++ b/docs/specs/integrations/README.md
@@ -44,6 +44,7 @@ outcomes that expose those contracts.
- [External Question Answering — authorized, discoverable, idempotent clarification resolution](requirements/external-question-answering.md)
- [Workspace GitHub Authentication](requirements/github-authentication.md)
- [GitHub PR Merge Queue](requirements/github-pr-merge-queue.md)
+- [GitHub PR Merge Queue Recovery](requirements/github-pr-merge-queue-recovery.md)
- [GitLab Integration](requirements/gitlab-integration.md)
- [GitLab MR Status Chip](requirements/gitlab-mr-status-chip.md)
- [GitLab MR Badge on the Sidebar and Tasks-List Rows](requirements/gitlab-mr-task-list-badges.md)
@@ -70,6 +71,7 @@ outcomes that expose those contracts.
- [Workspace GitHub Authentication System Design Part 2](system-design/github-authentication-02.md)
- [Workspace GitHub Authentication System Design Part 3](system-design/github-authentication-03.md)
- [GitHub PR Merge Queue](system-design/github-pr-merge-queue.md)
+- [GitHub PR Merge Queue Recovery](system-design/github-pr-merge-queue-recovery.md)
- [GitLab Integration System Design Part 1](system-design/gitlab-integration-01.md)
- [GitLab Integration System Design Part 2](system-design/gitlab-integration-02.md)
- [GitLab MR Status Chip System Design Part 1](system-design/gitlab-mr-status-chip-01.md)
diff --git a/docs/specs/integrations/requirements/github-pr-merge-queue-recovery.md b/docs/specs/integrations/requirements/github-pr-merge-queue-recovery.md
new file mode 100644
index 0000000000..ba76cd3bcf
--- /dev/null
+++ b/docs/specs/integrations/requirements/github-pr-merge-queue-recovery.md
@@ -0,0 +1,124 @@
+---
+status: draft
+system: integrations
+created: 2026-08-24
+owners:
+ - Kandev
+---
+# GitHub PR Merge Queue Recovery Requirements
+
+## Overview
+
+GitHub can remove a pull request from a merge queue after a merge-group check
+fails or a conflict appears. Kandev shows active queue membership, but it does
+not retain the removal event or repair the pull request.
+
+Queue-aware automation extends the existing per-PR auto-fix and auto-merge
+controls. It does not add a separate queue-recovery preference.
+
+## Terminology
+
+- **Queue attempt:** One queue entry for one pull-request head commit.
+- **Queue removal:** A GitHub `RemovedFromMergeQueueEvent` for an open pull
+ request.
+- **Actionable removal:** A queue removal with a failed check, a timed-out
+ check, or a merge conflict.
+
+## Requirements
+
+### REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001: Queue removal observation
+
+**Intent:** Kandev must retain a queue removal after the active queue entry
+disappears. This record lets automation respond after a short queue attempt.
+
+#### Acceptance criteria
+
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.1:** When GitHub reports
+ a queue removal, Kandev shall store its provider event ID, time, reason, and
+ available commit identity.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.2:** When Kandev misses
+ the active queue entry, a later poll shall still detect the latest queue
+ removal from the pull-request timeline.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.3:** When Kandev
+ restarts, the last observed queue attempt and removal shall remain available
+ for deduplication and status display.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001.4:** When a queue-aware
+ read fails, Kandev shall preserve the last complete queue-recovery state.
+
+### REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002: Queue failure auto-fix
+
+**Intent:** Auto-fix must send actionable merge-queue failures to the linked
+task agent without sending the same removal more than once.
+
+#### Acceptance criteria
+
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.1:** When auto-fix is
+ enabled and a new actionable removal appears, Kandev shall send or queue one
+ auto-fix prompt for that removal.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.2:** The prompt shall
+ include the queue-removal reason and conflict state. It shall include failed
+ check names and links only when Kandev has an exact commit identity for those
+ checks.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.3:** The removal event ID
+ shall participate in the auto-fix checkpoint. Repeated polls shall not send
+ the same removal again.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.4:** A queue-recovery
+ prompt shall consume one of the existing 10 auto-fix rounds for that linked
+ pull request.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.5:** A manual or unknown
+ removal without failed-check or conflict evidence shall not start auto-fix.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.6:** When the task is
+ busy, the queue-recovery prompt shall use the existing durable auto-fix queue
+ and coalescing rules.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.7:** When a user enables
+ auto-fix after Kandev retained an unhandled actionable removal for the
+ current head, the next evaluation shall send or queue one repair prompt.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002.8:** Kandev shall not
+ treat the removal event's commit as a merge-group commit unless GitHub
+ identifies it as such.
+
+### REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003: Safe automatic requeue
+
+**Intent:** Auto-merge must requeue a repaired pull request without creating a
+same-head retry loop or reversing an intentional queue removal.
+
+#### Acceptance criteria
+
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.1:** When GitHub accepts
+ a queue attempt, Kandev shall record the attempted pull-request head commit.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.2:** When GitHub removes
+ that queue attempt, auto-merge shall not requeue the same head commit.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.3:** When the head commit
+ changes and the existing merge gates pass, auto-merge shall submit one new
+ queue-aware merge request.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.4:** A user or agent can
+ repair the pull request when auto-fix is disabled. A later eligible head
+ commit shall still re-arm auto-merge.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.5:** When both controls
+ are enabled, each actionable removal can start one repair round. A new
+ eligible head can then start one queue attempt.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.6:** Kandev shall use
+ GitHub's queue-aware default merge action. It shall not bypass repository
+ rules or choose direct merge instead of the required queue.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.7:** When a user enables
+ auto-merge while the pull request already has an active queue entry, Kandev
+ shall adopt that entry and head as the current attempt. It shall not submit a
+ duplicate merge request.
+- **AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003.8:** When a user enables
+ auto-merge after a queue removal, Kandev shall keep the same-head guard. It
+ shall wait for a new eligible head instead of reversing the removal.
+
+## Related requirements
+
+- [GitHub PR Merge Queue](github-pr-merge-queue.md)
+- [Task PR Automation Controls](../../ui/requirements/ci-pr-automation.md)
+- [Merge Queue Recovery Controls](../../ui/requirements/ci-pr-merge-queue-recovery-controls.md)
+
+## Out of scope
+
+- Webhook ingestion for merge-group events.
+- Automatic retry of a flaky queue failure without a new pull-request head.
+- Editing branch protection, rulesets, or workflow files from the controls.
+- Removing a pull request from the queue.
+- Coordinated recovery and queue ordering for stacked pull requests.
+- GitLab merge-request behavior.
diff --git a/docs/specs/integrations/system-design/github-pr-merge-queue-recovery.md b/docs/specs/integrations/system-design/github-pr-merge-queue-recovery.md
new file mode 100644
index 0000000000..763a79e9c3
--- /dev/null
+++ b/docs/specs/integrations/system-design/github-pr-merge-queue-recovery.md
@@ -0,0 +1,203 @@
+---
+status: draft
+system: integrations
+requirements:
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002
+ - REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003
+created: 2026-08-24
+owners:
+ - Kandev
+---
+# GitHub PR Merge Queue Recovery System Design
+
+## Purpose and boundaries
+
+This design extends the existing GitHub PR poller and CI automation evaluator.
+It retains queue-removal evidence, sends repair prompts, and re-arms auto-merge
+after a new pull-request head appears.
+
+GitHub remains the source of truth for queue policy and merge eligibility.
+Kandev keeps the existing one-minute poll interval and queue-aware merge API.
+
+## Requirement mapping
+
+| Requirement | Design section |
+| --- | --- |
+| `REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-001` | [Provider observation](#provider-observation), [Persistence](#persistence) |
+| `REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-002` | [Auto-fix flow](#auto-fix-flow) |
+| `REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-RECOVERY-003` | [Requeue flow](#requeue-flow) |
+
+## Provider observation
+
+`apps/backend/internal/github/graphql.go` extends the batched pull-request
+selection with these fields:
+
+```graphql
+headRefOid
+mergeQueueEntry {
+ id
+ state
+ position
+ estimatedTimeToMerge
+ headCommit { oid }
+}
+timelineItems(last: 1, itemTypes: REMOVED_FROM_MERGE_QUEUE_EVENT) {
+ nodes {
+ ... on RemovedFromMergeQueueEvent {
+ id
+ createdAt
+ reason
+ beforeCommit { oid }
+ }
+ }
+}
+```
+
+The timeline event remains available after `mergeQueueEntry` becomes `null`.
+This property closes the gap between queue attempts and the one-minute poll.
+
+The GraphQL result records separate populated flags for queue membership and
+queue-removal history. A REST or `gh` read cannot clear either observation.
+
+## Persistence
+
+`github_task_prs` adds these snapshot fields:
+
+- `head_sha` stores the current pull-request head.
+- `merge_queue_entry_id` stores the active queue entry ID.
+- `merge_queue_entry_head_sha` stores the head for the active queue attempt.
+- `merge_queue_last_removal_id` stores the latest removal event ID.
+- `merge_queue_last_removed_at` stores the provider event time.
+- `merge_queue_last_removal_reason` stores the provider reason.
+- `merge_queue_last_removal_before_sha` stores the optional event commit.
+
+An authoritative `mergeQueueEntry: null` clears only the active entry fields.
+The last removal fields remain until a newer removal replaces them.
+
+Every `TaskPR` write path must preserve these fields. The normal task-PR event
+publishes them to current automation and UI consumers.
+
+`github_task_ci_pr_state` adds these fields:
+
+- `last_queue_attempt_head_sha` stores the head from the last accepted or
+ observed queue attempt.
+- `last_queue_fix_event_id` stores the last removal that consumed an auto-fix
+ round.
+- `last_queue_removal_cause` stores the normalized cause for the latest
+ removal. Values are `checks_failed`, `checks_timed_out`, `conflict`,
+ `manual`, `branch_protection`, and `unknown`.
+
+## Auto-fix flow
+
+`handleTaskPRCIAutomationWithRefresh` evaluates queue evidence before normal PR
+feedback. A new removal is new when its event ID differs from
+`last_queue_fix_event_id`.
+
+The evaluator classifies a removal as actionable when one of these facts
+exists:
+
+- The provider reason matches a reviewed failure or timeout form.
+- The pull request has a merge conflict.
+- The active queue entry reached GitHub's `UNMERGEABLE` state.
+
+The reason mapper accepts only reviewed provider-generated forms. It maps
+manual removal, branch-protection failure, and all unknown text to a
+non-actionable cause. Structured logs record unknown forms for later review.
+
+GitHub documents `beforeCommit` as the commit before the removal event. It does
+not document this field as the temporary merge-group commit. The evaluator
+does not fetch check runs for this commit or use its checks as merge-group
+evidence.
+
+When Kandev has an exact merge-group commit from a supported provider source,
+the existing `ListCheckRuns` boundary can add failed check names and links.
+This polling-only version does not infer that identity from the removal event.
+
+The auto-fix checkpoint adds a queue-removal item with its event ID, normalized
+cause, raw reason, time, conflict state, and available check snapshots.
+Existing sanitization and `sysprompt.Wrap` boundaries treat all provider text
+as untrusted data.
+
+The existing auto-fix dispatch path owns session selection, durable queuing,
+coalescing, and the 10-round limit. One accepted queue-recovery prompt records
+`last_queue_fix_event_id` with the normal fix checkpoint.
+
+Enabling auto-fix runs the normal evaluator. If the latest retained removal is
+actionable, belongs to the current head, and has not been checkpointed, the
+evaluator accepts one repair round. Enabling the option while the pull request
+is still queued only arms this behavior for a later removal.
+
+## Requeue flow
+
+The merge signature adds `TaskPR.HeadSHA`. This change makes a new commit a
+clear re-arm signal, even when file counts and gate values stay unchanged.
+
+Kandev records the current merge signature and
+`last_queue_attempt_head_sha` when either condition occurs:
+
+- Kandev receives a successful `queued` merge outcome.
+- The poller observes an active queue entry that another client created.
+
+This observation also handles a user who enables auto-merge while the pull
+request is already queued. The evaluator adopts the active entry and returns
+without calling the merge API.
+
+When Kandev first observes a removal without an earlier active observation, it
+records a conservative baseline for the current head. This fallback prevents
+an immediate same-head requeue.
+
+The normal readiness gates remain unchanged. Auto-merge submits a new request
+only when the head SHA changes and all gates pass. The existing
+`MergePRForAutomation` call uses GitHub's `merge_action=default` behavior.
+Enabling auto-merge after removal does not weaken this condition.
+
+This state machine has these transitions:
+
+```text
+ready head A -> queued head A -> removed head A -> blocked head A
+blocked head A -> auto-fix or manual repair -> ready head B -> queued head B
+```
+
+The event ID deduplicates repair work. The head SHA deduplicates queue work.
+
+The option combinations produce these outcomes:
+
+- Auto-fix only sends one actionable removal to the agent. It does not requeue.
+- Auto-merge only adopts an active attempt or queues a later eligible new head.
+ It does not repair the failure or requeue the removed head.
+- Both options run the full repair-and-requeue flow.
+
+## Failure and recovery
+
+- If the timeline query fails, Kandev preserves the prior removal snapshot.
+- If the reason mapper does not recognize a value, Kandev records the value as
+ `unknown`. It does not start auto-fix from that value.
+- If removal evidence is not actionable, Kandev records the status only.
+- If the current head SHA is absent, Kandev does not requeue automatically.
+- If GitHub rejects a requeue request, the existing error row shows the error.
+- If a repair produces no new head, Kandev waits. A user can repair or requeue
+ the pull request on GitHub.
+
+## Permissions and security
+
+The timeline and check reads use the workspace automation identity. The merge
+request uses the existing workspace-scoped automation write identity.
+
+Kandev does not trust the removal reason, check name, check output, or URL as
+instructions. Prompt rendering keeps the existing untrusted-data boundary.
+
+## Observability
+
+Structured logs identify the task, repository, pull request, removal event,
+head SHA, normalized cause, action classification, and decision. Logs do not
+include check output.
+
+The existing per-PR `last_error` and automation-state event expose failed
+reads, dispatches, and queue requests to the automation surface.
+
+## Related decisions and designs
+
+- [GitHub PR Merge Queue requirements](../requirements/github-pr-merge-queue.md)
+- [Task PR Automation Controls design](../../ui/system-design/ci-pr-automation-01.md)
+- [PR agent notification decision](../../../decisions/0051-pr-agent-notifications-extend-task-pr-automation.md)
diff --git a/docs/specs/ui/README.md b/docs/specs/ui/README.md
index 6817a66a6f..c55308a372 100644
--- a/docs/specs/ui/README.md
+++ b/docs/specs/ui/README.md
@@ -50,6 +50,7 @@ contract that other capabilities can reuse.
- [Backend-owned cancel-turn progress](requirements/cancel-turn-progress.md)
- [Responsive Changes Walkthrough Action](requirements/changes-walkthrough-toolbar-width.md)
- [Task PR Automation Controls](requirements/ci-pr-automation.md)
+- [Merge Queue Recovery Controls](requirements/ci-pr-merge-queue-recovery-controls.md)
- [Clarification Shared Context](requirements/clarification-context.md)
- [Clarification submit feedback](requirements/clarification-submit-feedback.md)
- [Command-panel Sidebar Task Reveal](requirements/command-panel-sidebar-task-reveal.md)
@@ -149,6 +150,7 @@ contract that other capabilities can reuse.
- [Task PR Automation Controls System Design Part 1](system-design/ci-pr-automation-01.md)
- [Task PR Automation Controls System Design Part 2](system-design/ci-pr-automation-02.md)
- [Task PR Automation Controls System Design Part 3](system-design/ci-pr-automation-03.md)
+- [Merge Queue Recovery Controls](system-design/ci-pr-merge-queue-recovery-controls.md)
- [Entity Reference Composer](system-design/entity-reference-composer.md)
- [Persistent status motion](system-design/persistent-status-motion.md)
- [Prompt History Panel](system-design/prompt-history-panel.md)
diff --git a/docs/specs/ui/requirements/ci-pr-merge-queue-recovery-controls.md b/docs/specs/ui/requirements/ci-pr-merge-queue-recovery-controls.md
new file mode 100644
index 0000000000..99f6d370e8
--- /dev/null
+++ b/docs/specs/ui/requirements/ci-pr-merge-queue-recovery-controls.md
@@ -0,0 +1,78 @@
+---
+status: draft
+system: ui
+created: 2026-08-24
+owners:
+ - Kandev
+---
+# Merge Queue Recovery Controls Requirements
+
+## Overview
+
+The PR automation surface already separates repair from merge policy. Merge
+queue recovery uses those controls because it has the same two decisions.
+
+The surface must explain queue recovery without adding a third switch. It must
+also show the current recovery state on desktop and mobile.
+
+## Requirements
+
+### REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001: Queue-aware automation controls
+
+**Intent:** A user can understand and control queue recovery from the existing
+per-PR automation surface.
+
+#### Acceptance criteria
+
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.1:** The automation surface shall use
+ the existing auto-fix and auto-merge switches. It shall not add a separate
+ queue-recovery switch.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.2:** The auto-fix control shall keep
+ its current label. Its help shall state that actionable merge-queue failures
+ can start an auto-fix round.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.3:** The auto-merge control shall use
+ the label `Auto-merge or requeue when ready`.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.4:** The automation surface shall show
+ one compact merge-queue status line for the selected linked pull request.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.5:** The status line shall distinguish
+ an active queue entry, an actionable removal, an accepted repair request, a
+ same-head wait, and a new-head check wait.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.6:** The information control shall
+ explain the switch combinations and the same-head retry guard.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.7:** The task prompt editor shall
+ explain that `{{pr.feedback}}` can include queue-removal context and
+ available failed merge-group checks.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.8:** The desktop hover popover and the
+ mobile PR status drawer shall show the same controls, status, and errors.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.9:** On a phone, each switch row shall
+ keep a 44-pixel touch target. The status shall not cause horizontal document
+ overflow.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.10:** The section title and subtitle
+ shall distinguish normal automation, active merge-queue automation, and
+ recovery after removal.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.11:** The two switch labels shall keep
+ stable accessible names across queue states. Contextual supporting text may
+ explain what each saved option will do next.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.12:** When the selected pull request is
+ already queued, the surface shall explain that auto-fix watches for a future
+ queue failure and auto-merge will not create a second queue attempt.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.13:** After removal, the surface shall
+ explain whether Kandev will send the failure to the agent, wait for a new
+ commit, wait for checks, or stop because the needed option is disabled.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.14:** The removal subtitle shall use a
+ localized cause that Kandev classified. When no classified cause exists, it
+ shall use a localized generic removal message.
+- **AC-UI-CI-PR-MERGE-QUEUE-RECOVERY-001.15:** The surface shall not show the
+ raw provider reason as a localized label or use it as a switch name.
+
+## Related requirements
+
+- [GitHub PR Merge Queue Recovery](../../integrations/requirements/github-pr-merge-queue-recovery.md)
+- [Task PR Automation Controls](ci-pr-automation.md)
+
+## Out of scope
+
+- A new queue-management panel.
+- A manual requeue action in the automation surface.
+- A per-PR auto-fix prompt override.
+- GitLab control labels or behavior.
diff --git a/docs/specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md b/docs/specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
new file mode 100644
index 0000000000..a0b40e693f
--- /dev/null
+++ b/docs/specs/ui/system-design/ci-pr-merge-queue-recovery-controls.md
@@ -0,0 +1,173 @@
+---
+status: draft
+system: ui
+requirements:
+ - REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001
+created: 2026-08-24
+owners:
+ - Kandev
+---
+# Merge Queue Recovery Controls System Design
+
+## Purpose and boundaries
+
+This design extends the existing PR automation popover and phone drawer. The
+two existing switches control repair and queue submission.
+
+The UI does not own provider classification or retry policy. It renders the
+state from `TaskPR`, per-PR options, and per-PR automation state.
+
+## Requirement mapping
+
+| Requirement | Design section |
+| --- | --- |
+| `REQ-UI-CI-PR-MERGE-QUEUE-RECOVERY-001` | [Control composition](#control-composition), [Responsive behavior](#responsive-behavior) |
+
+## Control composition
+
+`PRCIAutomationControls` keeps this order:
+
+1. A contextual section title and selected PR subtitle.
+2. `Auto-fix CI and address comments`.
+3. `Auto-merge or requeue when ready`.
+4. One non-interactive merge-queue status line when queue context exists.
+5. The existing `Review follow-up` disclosure.
+6. The existing error row.
+
+No queue-recovery switch appears. Auto-fix owns agent repair work. Auto-merge
+owns direct merge, initial enqueue, and safe requeue.
+
+The title and subtitle use these contexts:
+
+| Context | Title | Subtitle example |
+| --- | --- | --- |
+| No active queue or removal | `Automation` | `Applies to PR #3000 only` |
+| Active queue entry | `Merge queue automation` | `PR #3000 is in the merge queue` |
+| Removed with a classified cause | `Merge queue recovery` | `PR #3000 was removed: checks failed` |
+| Removed with no classified cause | `Merge queue recovery` | `PR #3000 was removed from the merge queue` |
+
+The switch labels do not change with these contexts. They are the stable names
+of saved per-PR preferences. Short supporting text changes to explain the next
+effect:
+
+| Context | Auto-fix supporting text | Auto-merge supporting text |
+| --- | --- | --- |
+| Active queue entry | `Will repair failed queue checks or conflicts` | `Current queue attempt is already active` |
+| Removed, actionable, auto-fix off | `Enable to send this queue failure to the agent` | `Will requeue after a new commit passes checks` |
+| Removed, repair accepted | `Repair requested for this removal` | `Waiting for a new commit before requeue` |
+| Removed, not actionable | `No automatic repair for this removal` | `Will requeue after a new commit passes checks` |
+| New head, checks pending | `The fix created a new commit` | `Waiting for required checks` |
+
+When auto-merge is off, the last two auto-merge descriptions instead state
+that automatic requeue is off. Supporting text is not part of the switch's
+accessible name.
+
+`CIAutomationOptionRows` uses one pure presentation helper. The helper derives
+these states:
+
+- `queued`: An active queue entry exists.
+- `removed_actionable`: An actionable removal exists, but no repair was
+ accepted for it.
+- `removed_not_actionable`: A manual, branch-protection, or unknown removal
+ exists. Kandev shows the event without offering automatic repair as its next
+ result.
+- `repair_requested`: The latest removal has an accepted auto-fix round, and
+ the pull-request head is unchanged. The UI does not claim that the agent is
+ running because durable acceptance does not prove execution state.
+- `waiting_for_commit`: The latest queue attempt left the queue, auto-merge is
+ enabled, and the pull-request head is unchanged.
+- `waiting_for_checks`: The head changed after removal, auto-merge is enabled,
+ and one or more normal merge gates have not passed.
+- `none`: No active or retained recovery state needs a line.
+
+The helper can combine `repair_requested` and `waiting_for_commit` into
+`Repair requested. Waiting for a new commit.` A disabled option changes the
+supporting text, not the provider state.
+
+The status line uses translated labels and a muted semantic style. A destructive
+error continues to use `CIAutomationErrorRow`.
+
+The header information control explains these combinations:
+
+- Auto-fix only repairs an actionable queue removal.
+- Auto-merge only submits an eligible head.
+- Both controls form the repair-and-requeue loop.
+- Kandev never requeues the same head after removal.
+
+When automation is enabled on an already queued pull request, the visible
+active entry becomes the baseline. The UI updates the saved option immediately,
+but it does not show a new queue submission. Auto-fix watches for a later
+actionable removal. Auto-merge adopts the current attempt.
+
+The prompt dialog states that `{{pr.feedback}}` can include queue-removal
+context and available failed merge-group checks.
+
+## Data contracts
+
+`apps/web/lib/types/github.ts` adds the queue-recovery snapshot fields to
+`TaskPR`. It adds `last_queue_attempt_head_sha` and
+`last_queue_fix_event_id` to `TaskCIPRAutomationState`. It also adds the
+normalized `last_queue_removal_cause` enum.
+
+The subtitle and status use this normalized enum. They never insert the raw
+provider reason into a translation key or accessible name. If the enum is
+`unknown`, the UI uses the generic removal copy.
+
+The UI reads only existing boot, HTTP, and WebSocket data. It does not add a
+fetch from the component.
+
+## Responsive behavior
+
+### Desktop outcome and mobile entry point
+
+A fine pointer opens the existing hover popover. A phone user taps the existing
+PR status chip and uses the existing drawer.
+
+### Nearest mobile exemplar
+
+`PRStatusChipDrawer` is the nearest shipped surface. It supplies the inset
+drawer, internal scroll region, and safe-area behavior.
+
+### Information hierarchy and primary action
+
+The two switches remain the primary controls. The status line explains their
+current queue outcome. The PR status chip remains the mobile entry point.
+
+### Presentation rationale
+
+Queue recovery is short status content inside an existing temporary surface.
+It does not need a new route, drawer, or nested disclosure.
+
+### Geometry and shared logic
+
+`CIAutomationRow` keeps its 44-pixel coarse-pointer height. The status line can
+wrap inside the drawer, but it cannot create document-level horizontal scroll.
+The existing drawer remains the only vertical scroll owner.
+
+Desktop and mobile share state derivation, copy, and action handlers. Existing
+responsive wrappers continue to own presentation.
+
+## Accessibility
+
+The updated auto-merge label becomes the switch accessible name. The queue
+status line uses status text, not color alone.
+
+The information control remains reachable by keyboard and touch. Its copy
+names the same-head retry guard.
+
+## Localization
+
+All new labels and explanations use the `github` namespace. The change updates
+English, Portuguese, Simplified Chinese, Hong Kong Chinese, and Taiwan Chinese
+catalogs. The Traditional Chinese catalogs use `pnpm run i18n:zh-hant`.
+
+User-facing copy does not use a Unicode em dash.
+
+## Verification
+
+Focused component tests cover label changes, state derivation, and help copy.
+Desktop Playwright coverage uses the current hover popover. Mobile coverage
+uses a tap and the `mobile-chrome` project.
+
+The mobile test proves the 44-pixel switch rows, status visibility, internal
+drawer scroll, and no document-level horizontal overflow.