--- spec: docs/specs/integrations/requirements/github-pr-merge-queue.md created: 2026-08-22 status: done --- # Implementation Plan: GitHub PR Merge Queue Status ## Overview Extend the existing batched GitHub PR sync to observe and persist merge queue state, position, and estimated merge duration, then carry the queued semantic through the bounded task projection and the metadata through the existing PR status surfaces. Reuse the current task indicator, compact popover/drawer, and review detail compositions; no new polling path, endpoint, or queue-management action is needed. ## Backend ### GitHub queue observation - Update `apps/backend/internal/github/graphql.go` so `batchedPRResult` decodes `mergeQueueEntry { state position estimatedTimeToMerge }`, `prFieldsBlock` requests it for numbered and branch queries, and `convertBatchedPRResult` normalizes the provider enum while retaining GitHub's one-based position and optional estimate in seconds. - Update `apps/backend/internal/github/models.go` so `PRStatus` carries the normalized queue state, position, and estimate plus one internal populated guard, and `TaskPR` carries the three persisted queue fields. - Keep `newPRStatus` in `apps/backend/internal/github/client_helpers.go` queue-unaware. Its populated guard remains false, so REST and `gh pr view` feedback reads cannot erase a GraphQL observation. ### Persistence and synchronization - Add `merge_queue_state TEXT NOT NULL DEFAULT ''`, nullable `merge_queue_position INTEGER`, and nullable `merge_queue_estimated_time_to_merge_seconds INTEGER` to the fresh `github_task_prs` schema and the idempotent existing-database migration in `apps/backend/internal/github/store.go`. - Thread the field through `taskPRColumns`, `taskPRColumnsQualified`, `CreateTaskPR`, `ReplaceTaskPR`, `RestoreTaskPR`, and `UpdateTaskPR` so every association path round-trips the complete queue entry and schema replay remains safe. - Extend `taskPRSyncState`, `prepareTaskPRSyncState`, `taskPRChangedFields`, and `SyncTaskPR` in `apps/backend/internal/github/service_pr_watch.go`. A populated GraphQL result atomically replaces the stored queue state, position, and estimate; an unpopulated read preserves all three; and any merged or closed lifecycle state clears all three. - Extend `associateTaskPRRequest` and `buildTaskPRFromRequest` in `apps/backend/internal/github/mock_controller.go` so E2E fixtures can seed a queue state, position, and estimate and publish the normal `github.task_pr.updated` event. ### Bounded task status - Add merge queue state to the PR observation and rebuild input in `apps/backend/internal/task/statussummary`, including live event projection, restart rebuild, and aggregate-state derivation. - Add `queued` to the bounded PR aggregate states with a rank between `ready` and `passing`: a more attention-worthy sibling still wins for multi-PR tasks, while a single queued PR retains the queue color. - Pass `TaskPR.MergeQueueState` through `apps/backend/internal/backendapp/status_summary_adapter.go` so restart hydration and live events converge on the same aggregate state. ## Frontend ### Types and semantic color - Add the normalized `MergeQueueState` union and all three TaskPR queue fields to `apps/web/lib/types/github.ts`. - Add the shared `queued` semantic to `apps/web/components/integrations/change-request-task-status-color.ts`, using the exact `text-[#966600]` GitHub merge-queue color. This color is distinct from the current yellow CI-in-progress state. Add the same queued tone to the shared task-summary presentation. Preserve the existing worst-status aggregation for tasks with several open PRs. - Update `apps/web/components/github/pr-task-icon.tsx` and `apps/web/components/github/pr-status-chip.tsx` so an open queued PR exposes the queue color/status and a dedicated glyph before non-terminal review, check, and mergeability colors. Terminal states still win. ### Hover, compact status, and review detail - Extend the shared summary-row data in `apps/web/components/integrations/change-request-task-status-summary.tsx` with an optional localized detail line. Use it from the merge row in `apps/web/components/github/pr-task-status-summary.tsx` to show queue state, one-based position, and the available estimate. Recognized values distinguish queued, awaiting checks, mergeable, unmergeable, and locked; unknown non-empty values use generic queued copy. - Add a reusable queue-status notice under `apps/web/components/github/` that formats position and GitHub's estimate in localized minute/hour units. Values below 60 seconds use a localized sub-minute label; larger values round up to the next whole minute. Render it from `pr-mergeability-row.tsx` and `pr-detail-panel.tsx`. This places the same queue information in the compact desktop popover, phone status drawer, and full PR detail panel without creating another data fetch. - Add queue labels, descriptions, position, and pluralized duration copy to all five `apps/web/src/locales/*/github.json` catalogs. Generate `zh-hk` and `zh-tw` with `pnpm run i18n:zh-hant`. ### Mobile design contract - **Desktop outcome:** task and compact PR indicators use the queue color; hover/popover content and PR detail show translated queue state, position, and the available estimate. - **Mobile entry points:** tap the existing composer PR status chip for its drawer, or open the existing full-height Review destination. - **Nearest shipped exemplars:** `PRStatusChipDrawer` contributes the inset, internally scrolling status drawer; the existing mobile Review surface contributes direct navigation and full-height review geometry. - **Information hierarchy:** queue status replaces the mergeability line for an active entry and sits below review/check information in compact status; position and estimate form one subordinate metadata line. The detail notice stays below the PR header and above review content. - **Presentation and rationale:** retain the current drawer and direct Review navigation because this is content-only status inside already established surfaces, not a new temporary choice or primary destination. - **Geometry and shared logic:** no scroll owner, safe-area, breakpoint, or touch-target changes. Queue derivation, copy, and state are shared between viewports; only the existing responsive wrappers differ. ## Tests - **What:** GraphQL selection and decoding distinguish every known queue enum, position, a present or absent estimated duration, `null`, and an unknown future state. **File:** `apps/backend/internal/github/graphql_merge_queue_status_test.go`. **How:** query snapshot and table-driven conversion tests. - **What:** sync atomically persists queue entry transitions and metadata, preserves all fields across queue-unaware feedback reads, clears them on an authoritative `null`, and clears them for terminal PRs. **File:** `apps/backend/internal/github/service_pr_merge_queue_status_test.go`. **How:** focused service tests against the real store. - **What:** fresh schema, same-database replay, and every TaskPR write/read path round-trip the complete queue entry. **Files:** `apps/backend/internal/github/store_taskpr_schema_drift_test.go` and `apps/backend/internal/github/store_merge_queue_status_test.go`. **How:** real SQLite store tests. - **What:** live projection and restart rebuild derive the same `queued` aggregate and preserve multi-PR attention ranking. **Files:** `apps/backend/internal/task/statussummary/projector_test.go`, `apps/backend/internal/task/statussummary/rebuild_test.go`, and `apps/backend/internal/backendapp/status_summary_adapter_test.go`. **How:** table-driven projection and adapter tests. - **What:** queue membership maps to `#966600` on the task icon and compact chip. The hover merge row, compact queue notice, and detail notice show the translated status and metadata. Terminal and non-queued states retain their existing behavior. **Files:** `apps/web/components/github/pr-task-icon.test.ts`, `pr-task-status-summary.test.ts`, `pr-status-chip.test.tsx`, `pr-merge-queue-status.test.tsx`, `pr-mergeability-row.test.tsx`, and `pr-detail-panel.test.tsx`. **How:** pure formatter and focused component tests, including absent, sub-minute, minute, and hour estimate boundaries. ## E2E Tests - **Scenario:** GIVEN an eligible queue-required PR, WHEN a desktop user activates `Merge PR`, THEN the merge API returns `{"status":"queued"}`, the success notification appears, and the accepted state suppresses the duplicate action. **File:** the action test in `apps/web/e2e/tests/pr/pr-merge-queue.spec.ts`. - **Scenario:** GIVEN a linked open PR with an active merge queue entry, WHEN a desktop user views the task and PR detail, THEN the task indicator has queued semantics, its hover summary and compact popover show queue state, position, and estimated merge duration, and the detail notice shows the same data. **File:** the display test in `apps/web/e2e/tests/pr/pr-merge-queue.spec.ts`. - **Scenario:** GIVEN an eligible queue-required PR on a phone viewport, WHEN the user opens Review and activates the action, THEN the queued notification appears, the action target is at least 44px high, and the document has no horizontal overflow. **File:** the action test in `apps/web/e2e/tests/pr/mobile-pr-merge-queue.spec.ts`. - **Scenario:** GIVEN the same queued PR on a phone viewport, WHEN the user opens the PR status drawer and the Review destination, THEN both expose queue status, position, and the available estimate without hover, and the document retains no horizontal overflow. **File:** the display test in `apps/web/e2e/tests/pr/mobile-pr-merge-queue.spec.ts`. ## Verification Results Passed: - `cd apps/backend && go test -tags fts5 ./internal/github/... ./internal/task/statussummary/... ./internal/backendapp` passed 2,193 tests across 3 packages. The focused RED run recorded the expected missing-contract compile failures, and the focused GREEN run passed the queue precedence tests. - `cd apps/web && pnpm test -- components/github/pr-task-icon.test.ts components/github/pr-task-status-summary.test.ts components/github/pr-status-chip.test.tsx components/github/pr-merge-queue-status.test.tsx components/github/pr-mergeability-row.test.tsx components/github/pr-detail-panel.test.tsx` passed 160 tests across 6 files. - `cd apps/web && pnpm run typecheck` and `cd apps/web && pnpm run lint` passed. `cd apps/web && pnpm run i18n:check` passed with 7,223 referenced keys, 8,779 English entries, 48 orphans, and all five required catalogs complete. - `node scripts/validate-public-docs.test.mjs` passed 61 tests, and `node scripts/validate-public-docs.mjs` validated 41 published pages. `docs/public/integrations.md` documents queue state, position, and estimate surfaces. - A fresh `cd apps/web && pnpm e2e:run tests/pr/pr-merge-queue.spec.ts` build completed the backend, Vite production bundle, and plugin. The final desktop run passed 2 tests, including the merge-action and queue-display scenarios, and `pnpm e2e:run --no-build --project mobile-chrome tests/pr/mobile-pr-merge-queue.spec.ts` passed 2 matching tests. - Previously captured display assets remain valid: `apps/web/.pr-assets/pr-merge-queue--desktop-pr-merge-queue-status.png` and `apps/web/.pr-assets/mobile-pr-merge-queue--mobile-pr-merge-queue-status.png`. Both were inspected, compressed with the `pngquant-bin` fallback, and preserved for PR media publication; action coverage is behavioral and adds no capture asset. - `cd apps/web && pnpm e2e:clean` removed E2E results, reports, PR assets, and shard logs. The E2E scenarios use mock GitHub state and created no external GitHub writes. ## Implementation Waves And Parallel Candidates Wave 1: - [x] [Task 01: Backend queue-state projection](task-01-backend-queue-state.md) Wave 2: - [x] [Task 02: Frontend queued PR status](task-02-frontend-queued-status.md) Wave 3: - [x] [Task 03: Responsive merge-queue E2E](task-03-responsive-merge-queue-e2e.md) Execution is sequential in the primary conversation unless the user explicitly authorizes subagents.