ADE's pull-request surface manages lane-backed PRs, stacked PR chains, PR merge queues, integration (merge-plan) proposals, and GitHub inspection. It treats local git state as the source of truth for merge/integration simulation while keeping remote GitHub state warm through layered caching.
This folder documents:
stacking.md— stacked PR chains, rebase ordering, queue-aware rebase targeting.queue.md— PR merge queue model and landing state machine.conflict-simulation.md— how ADE predicts PR merge conflicts before the user hits Merge.path-to-merge.md— the Path-to-Merge orchestrator: phase delays, terminal-state gate, conflict strategy switch, force-finalize, merge ladder, and Queue Automate Merging.
PR CRUD, GitHub polling, queue landing, integration proposal
simulation, the Path-to-Merge orchestrator, and the issue/rebase
resolver agent dispatch all run inside the active ADE runtime
(local daemon for local-bound windows, SSH-attached remote runtime
for remote-bound windows). The renderer's window.ade.prs.* surface
in apps/desktop/src/preload/preload.ts routes every PR call through
callProjectRuntimeActionOr("pr", …) and falls back to the legacy
in-process IPC handlers only when no runtime is bound. PR polling
fingerprints, the prsRouteState.ts URL-state helper, and the
PR detail panes are renderer-only — they hold no service state.
For remote-bound windows, GitHub polling, the queue automation loop, and the Path-to-Merge orchestrator all execute on the remote machine. The git operations that back PR merges, rebases, and conflict resolution use the worktrees on the remote host. Stop / start / status reads work exactly the same as local; the desktop window just sends every action through the SSH-tunneled JSON-RPC instead of the local socket.
Services. The canonical implementations run inside the runtime daemon; the desktop main-process files below stay as fallback targets for the legacy in-process IPC path.
Service files (apps/desktop/src/main/services/prs/):
| File | Responsibility |
|---|---|
prService.ts |
PR CRUD, GitHub sync, merge context, draft descriptions, check/review/comment hydration, cached detail snapshots (listSnapshots), commit snapshots (getCommits), integration proposals, merge-into-existing-lane adoption, merge bypass, post-merge cleanup, standalone PR branch cleanup (cleanupBranch), deployment listing, review-thread reply/resolve/react mutations for the timeline, the aggregate getMobileSnapshot that powers the iOS PRs tab, and listOpenPullRequests — a paginated /repos/{owner}/{name}/pulls?state=open fetch returning BranchPullRequest[] for the lane-creation branch picker. getForLane(laneId) resolves through getDisplayRowForCurrentLaneBranch: it returns the most recently updated PR whose head branch matches the lane's current branch ref, ranked open/draft → merged → closed, so a freshly merged PR still shows in lane-scoped UI instead of disappearing the moment GitHub flips the state. |
prService.mobileSnapshot.test.ts |
Coverage for the mobile snapshot builder: stack chaining, capability gates, per-lane create eligibility, workflow-card aggregation |
prService.mergeInto.test.ts |
Coverage for integration proposals that preview or adopt an existing merge target lane, including dirty-worktree handling and drift metadata. |
prPollingService.ts |
60 s polling loop, fingerprint-based change detection, notification emission. Writes last_polled_at per PR so callers can run delta polls on the next tick |
prSummaryService.ts |
AI PR summary generator; caches PrAiSummary per (prId, headSha) in pull_request_ai_summaries so pushes invalidate the cache |
queueLandingService.ts |
Merge queue state machine (ALLOWED_TRANSITIONS), landing loop, auto-resolve on conflicts |
pathToMergeOrchestrator.ts |
Path-to-Merge orchestrator: phase-aware setTimeout wake-ups, combined CI + review terminal-state gate, 4-option conflict strategy, force-finalize bonus iteration, REST → gh --admin → gh --auto merge ladder, persistent resume across restarts. Native port of the /shipLane Claude skill. See path-to-merge.md. |
integrationPlanning.ts |
buildIntegrationPreflight — validates source lanes for an integration proposal |
integrationValidation.ts |
parseGitStatusPorcelain, hasMergeConflictMarkers — shared helpers for integration flows |
issueInventoryService.ts |
Typed issue inventory, per-round convergence status, participant classification, thread re-open logic. IssueInventoryItem carries type (`review_thread |
prIssueResolver.ts |
Builds issue-resolution prompts for the agent, launches chat session |
prRebaseResolver.ts |
Builds rebase-resolution prompts, launches chat session |
resolverUtils.ts |
Shared permission-mode mapping, recent commit reading, comment noise filter, and the looksLikeResolutionAck heuristic that flags resolved-looking replies on unresolved review threads |
Renderer components (apps/desktop/src/renderer/components/prs/):
| File | Responsibility |
|---|---|
PRsPage.tsx |
Top-level tab shell (GitHub vs Workflows) with URL-driven state |
state/PrsContext.tsx |
PR data provider (list, selection, queue groups, rebase needs, convergence runtime state) |
prsRouteState.ts |
URL ↔ page state mapping |
CreatePrModal.tsx |
Draft/queue/integration PR creation with lane warnings, branch name validation |
tabs/NormalTab.tsx |
Normal PR list |
tabs/GitHubTab.tsx |
Unified repo + external PR browser with label filters, CI badges, review indicators |
tabs/QueueTab.tsx |
Merge queue UI. Hosts the "Automate Merging" entry point that opens QueueAutomateMergingModal with the queue's eligible members (everything that has not landed yet). |
tabs/QueueAutomateMergingModal.tsx |
Stack-wide automation modal: edits one PipelineSettings config that applies to every queue member, then sequentially saves settings, calls ade.prs.retargetBase for non-leading members so each PR's base points at the queue's tracking branch, starts Path-to-Merge via ade.prs.pathToMerge.start, and polls convergenceStateGet every 4 s until the runtime status is terminal. Halts the sequence on the first `failed |
tabs/IntegrationTab.tsx |
Integration (merge-plan) proposals and execution, including merge-into-lane selection, apply-and-resimulate, and adopted-lane cleanup messaging |
tabs/RebaseTab.tsx |
Lane rebase needs (base + queue + PR target) and attention items |
tabs/WorkflowsTab.tsx |
Container for queue/integration/rebase sub-tabs |
tabs/queueWorkflowModel.ts |
Pure model for queue tab rendering (active/history bucketing, guidance computation) |
detail/PrDetailPane.tsx |
Selected PR detail pane: status, checks, reviews, comments, merge readiness, bypass, Path-to-Merge convergence sub-tab (labelled "Path to Merge" in the tab list), resolver modals. Switches the Overview tab between the legacy grid and the Timeline+Rails layout based on prsTimelineRailsEnabled. Persists the selected sub-tab (`overview |
detail/PrDetailTimelineRails.tsx |
Timeline+Rails overview: merges timeline events, commit rail (seeded from both PrActivityEvent.commit_push entries and the getCommits snapshot), status rail, deployment cards, AI summary, and command-palette navigation (g c / g t / g f and [ / ]) |
shared/PrTimeline.tsx |
Timeline column: synthesises PrTimelineEvents from detail data, handles per-PR filters (PrTimelineFilters), renders grouped events |
shared/PrCommitRail.tsx, shared/PrStatusRail.tsx |
Right-hand rails on the timeline view: commit list, checks/reviews summary, deployment chips |
shared/PrCommandPalettes.tsx |
g c (commits) / g t (threads) / g f (files) palettes opened by the keyboard chord and by the timeline toolbar |
shared/PrAiSummaryCard.tsx |
AI summary card above the timeline; dismissible per PR (state in PrsContext.dismissedAiSummaries), with a "Regenerate" action wired to prSummaryService.regenerateSummary |
shared/PrReviewThreadCard.tsx, shared/PrBotReviewCard.tsx |
Rich thread cards for the timeline (bot-review collapse, reply box, resolve/react actions) |
shared/PrDeploymentCard.tsx |
Deployment row used in the status rail and on the timeline |
shared/PrConvergencePanel.tsx |
Path-to-Merge slide-over panel with issue inventory, agent session embed, pipeline settings. Status copy uses "Path to Merge" verbatim (e.g. "Agent working on Path to Merge…", "Ready to launch another Path to Merge run"). Each issue row is expandable (caret toggles full comment body, author, and thread comment count); a "show ignored" toggle un-hides previously dismissed items. The dismiss button is labelled "Ignore comment" so users understand it removes the item from the round without resolving the thread. The waiting-state copy hides the round number when the panel runs in non-round-aware contexts (showRoundLabels = false). Terminal PRs render a frozen state with historical comments shown for reference only. |
shared/PrIssueResolverModal.tsx |
Launch issue resolution (checks/comments/both scopes) |
shared/PrAiResolverPanel.tsx |
AI resolver launch controls in Rebase/Integration flows, including additional-instructions passthrough |
shared/PrPipelineSettings.tsx |
Per-PR pipeline settings editor used inside PrConvergencePanel and the queue Automate Merging modal. Surfaces the 4-option conflictStrategy selector, the auto-only autoAgentSettings group (provider / model / reasoning / permission mode / confidence threshold), the forceFinalizeMode selector with the conditional sub-toggle, the earlyMergeOnGreen switch, autoMerge, mergeMethod, and maxRounds. Renders a force-push warning when conflictStrategy is rebase or auto. |
shared/PrLaneCleanupBanner.tsx |
Post-merge cleanup banner on the PR detail. Also renders a dedicated "PR branch cleanup" variant when the PR is linked to the primary lane but its head branch differs — the primary lane is never deleted, but the user can still delete the local and/or remote PR branch after confirming delete <branch> |
shared/IntegrationPrContextPanel.tsx |
Integration PR context panel |
shared/prVisuals.tsx |
CI running indicator, check/review badges, dot colors, activity derivation |
shared/rebaseNeedUtils.ts |
Rebase need dedup, route selection, upstream rebase chain |
shared/rebaseAttentionUtils.ts |
Auto-rebase attention items for the Rebase tab |
shared/lanePrWarnings.ts |
Pre-submit lane-health warnings |
shared/prFormatters.ts |
Formatting helpers shared across PR surfaces. formatPrBadgeLabel(pr) returns a state-aware compact badge (PR #123, DRAFT #123, MERGED #123, CLOSED #123) used by the chat git toolbar and the lane list PR tag so closed/merged PRs aren't visually identical to open ones. |
shared/laneBranchTargets.ts |
Target branch resolution for PR creation |
ConflictFilePreview.tsx |
File-level conflict marker preview |
PrRebaseBanner.tsx |
Rebase banner on a PR |
PrConflictBadge.tsx |
Lightweight conflict chip |
Shared contracts:
| File | Responsibility |
|---|---|
apps/desktop/src/shared/types/prs.ts |
PR DTOs and integration proposal contracts, including preferredIntegrationLaneId, mergeIntoHeadSha, integrationLaneOrigin, and additionalInstructions fields. |
apps/desktop/src/shared/types/git.ts |
BranchPullRequest (branch / prNumber / title / state / url / author / updatedAt) — the lightweight PR shape returned by prService.listOpenPullRequests and consumed by the branch picker without going through PrSummary. |
apps/desktop/src/shared/types/conflicts.ts |
Conflict resolver DTOs; PrepareResolverSessionArgs.additionalInstructions is appended to generated resolver prompts. |
apps/desktop/src/shared/ipc.ts / apps/desktop/src/preload/preload.ts |
PR IPC constants and renderer bridge for proposal simulation, update, commit, resolver, and cleanup flows. |
PrSummary (selected fields, full type in src/shared/types.ts):
type PrSummary = {
id: string;
laneId: string;
projectId: string;
repoOwner: string;
repoName: string;
githubPrNumber: number;
githubUrl: string;
title: string;
state: PrState; // open | closed | merged
baseBranch: string;
headBranch: string;
checksStatus: PrChecksStatus; // passing | failing | pending | unknown
reviewStatus: PrReviewStatus; // approved | changes_requested | review_required | ...
labels: PrLabel[];
isBot: boolean;
commentCount: number;
lastSyncedAt: string | null;
createdAt: string;
updatedAt: string;
};PrStatus adds live fields not cached on the summary row
(mergeability, behind-by, merge conflicts, activity events).
Selected channels exposed through preload.ts:
ade.prs.createFromLane,ade.prs.createQueue,ade.prs.createIntegrationade.prs.listAll,ade.prs.listProposals,ade.prs.listQueueStatesade.prs.listOpenForRepo— flat list of open PRs in the project's GitHub repo asBranchPullRequest[](branch / number / title / state / url / author / updatedAt). Independent ofpull_requestscache so the lane-creation branch picker can attach PR pills to branches that have no lane yet. See features/lanes/README.md for the consumer.ade.prs.land,ade.prs.landStack,ade.prs.landStackEnhanced,ade.prs.landQueueNextade.prs.getMergeContext,ade.prs.getMergeContexts,ade.prs.listSnapshots,ade.prs.getStatus,ade.prs.getChecks,ade.prs.getReviews,ade.prs.getComments,ade.prs.getFiles,ade.prs.getCommitsade.prs.cleanupBranch— delete a merged/closed PR's local and/or remote branch without touching the lane (protected against deleting any primary-lane branch)ade.prs.updateDescription,ade.prs.updateTitle,ade.prs.updateBody,ade.prs.setLabels,ade.prs.requestReviewers,ade.prs.submitReview,ade.prs.close,ade.prs.reopenade.prs.getReviewThreads,ade.prs.replyToReviewThread,ade.prs.resolveReviewThreadade.prs.postReviewComment,ade.prs.setReviewThreadResolved,ade.prs.reactToComment— GraphQL-backed mutations used by the timeline's thread cardsade.prs.getDeployments— deployments for the PR's head SHA, with the latest status status URL and environment URLade.prs.getAiSummary/ade.prs.regenerateAiSummary— cached/forcedPrAiSummaryper(prId, headSha)ade.prs.launchIssueResolutionFromThread— launch an agent chat pre-focused on a specific review thread (used by the thread card's "Resolve with agent" action)ade.prs.issueResolutionStart,ade.prs.issueResolutionPreviewade.prs.rebaseResolutionStartade.prs.convergenceStateGet,ade.prs.convergenceStateSave,ade.prs.convergenceStateDeleteade.prs.pathToMerge.start,ade.prs.pathToMerge.stop— drive the Path-to-Merge orchestrator (seepath-to-merge.md)ade.prs.retargetBase— re-point a PR's base branch (used by Queue Automate Merging when stacking the chain bases before PtM picks them up)ade.prs.pipelineSettingsGet,ade.prs.pipelineSettingsSave,ade.prs.pipelineSettingsDeleteade.prs.getGitHubSnapshot— merged repo + external PR snapshot. The default fetch includes open external PRs only; closed/merged external history is opt-in withincludeExternalClosed.ade.prs.simulateIntegration,ade.prs.createIntegrationLaneForProposal,ade.prs.commitIntegration,ade.prs.cleanupIntegrationWorkflow
Integration merge-into flow uses these existing channels with widened DTOs:
ade.prs.simulateIntegrationacceptsmergeIntoLaneId. Pairwise child-vs-child checks still usebaseBranch, while the sequential preview starts at the selected lane's current HEAD and returnsmergeIntoHeadSha.ade.prs.updateIntegrationProposalcan setpreferredIntegrationLaneId, storemergeIntoHeadSha, and clear an existing integration binding when the merge target changes.ade.prs.createIntegrationLaneForProposalandade.prs.commitIntegrationacceptallowDirtyWorktree; commit can also receivepreferredIntegrationLaneIdto override the stored preference.ade.prs.aiResolutionStartand issue-resolution launch args acceptadditionalInstructions, which are appended to the generated resolver prompt after the structured context.
The GitHub tab renders a unified list of repo PRs and external PRs
involving the current user, sorted by creation date. A scope filter
(all / ade / external) replaces the previous separate toggle.
Caching layers:
- Runtime cache — GitHub snapshot is cached for a short TTL
inside
prServiceon the active runtime (local daemon or remote-attached); repeated in-flight snapshot requests are deduplicated. The default snapshot fetches open external PRs only; closed/merged external history is requested after the user switches to a history filter or explicitly refreshes that view. - Renderer cache —
PrsContextholds the last snapshot so revisiting the tab renders immediately. Selected PR detail panes hydrate fromlistSnapshots({ prId })before live status, check, review, and comment requests run in the background. - Manual sync — a "Refresh" action forces a fresh pull. Explicit multi-PR refreshes run with bounded parallelism instead of refreshing each PR serially.
Snapshot contents include labels (name, color, description),
isBot, and commentCount fields so filters can run locally.
PR rows in tabs/GitHubTab.tsx and queue member rows in tabs/QueueTab.tsx
render the linked lane's color through LaneAccentDot (resolved from the
app store via useLaneColorById / a Map<laneId, color>); the rest of the
row text inherits the lane color so a glance correlates a PR with its lane
across the queue / GitHub / Workflows tabs.
getStatus() in apps/desktop/src/main/services/github/githubService.ts
returns a GitHubStatus shaped to be the single source of truth for
"GitHub is usable here" — UI banners and badges read status.connected
rather than re-deriving from individual fields.
Fields:
tokenStored,tokenDecryptionFailed,tokenType—classic|fine-grained|unknown. Set from token prefix on save.userLogin,scopes,checkedAt— outcome ofvalidateToken(callsGET /user). Classic tokens populatescopesfromx-oauth-scopes; fine-grained tokens never return that header soscopesis empty.repo— auto-detected origin owner/name.repoAccessOk: boolean | null,repoAccessError: string | null— result of an explicitGET /repos/{owner}/{name}probe (probeRepoAccess).nullmeans no probe was run (no repo to probe, orgetStatusreturned early on a token-error path).connected: boolean— computed bycomputeConnected:falseif token is missing oruserLoginis null.- For
fine-grainedtokens: requires the repo probe to pass (or no repo to probe). This is the only reliable check because fine-grained permissions are not introspectable from headers; a token can authenticate as a user yet 403 every PR-tab call. - For
classictokens: requiresgetGitHubTokenAccessState(scopes)to reporthasRequiredAccess. - For
unknowntoken prefixes: best-effort —userLoginis enough.
Status is cached in-memory for 30 s. The cache is bypassed when the
caller passes getStatus({ forceRefresh: true }) (Settings'
"REFRESH" button does this so the user can fix permissions on
github.com and immediately re-check). When the cache is hit but the
auto-detected repo has changed, repoAccessOk is reset to null
because the cached probe no longer applies.
Status changes broadcast through the ade.github.statusChanged IPC
channel (window.ade.github.onStatusChanged) every time
setToken / clearToken is called. AppShell subscribes so the
unconnected-banner state reflects the latest status the moment
Settings saves a new token — fixing the prior bug where Settings said
CONNECTED while the AppShell banner still said disconnected.
renderer/components/settings/GitHubSection.tsx distinguishes:
tokenAuthenticated— token decrypted anduserLoginis populated.isConnected(status.connectedfrom the backend) — the actual "GitHub is usable" gate. Drives the green CONNECTED / amber LIMITED ACCESS / muted NOT CONNECTED label and any saved-and-verified notice.- A repo-probe-failed inline error renders when the token authenticated but the probe came back 403/404, with copy that asks the user to grant Contents (Read), Pull requests (Read and write), and Metadata (Read) on the active repo (fine-grained tokens) or to make sure the classic token has access to the repo.
AppShell.describeGithubBanner(status) mirrors the same three-way
split for the banner copy: "not connected" / "cannot access
{owner}/{repo}" / "missing required permissions".
prPollingService runs at a 60 s default interval (clamped to
5 s–5 min, jittered ±10%). Each tick:
- Pulls the current PR list via
prService. - Computes a fingerprint per PR (excluding volatile timing fields:
lastSyncedAt,createdAt,updatedAt,projectId). - Diffs against last seen fingerprints; only changed PRs trigger events/UI updates.
- Emits
PrEventPayloadfor state transitions (checks failing, review requested, changes requested, merge ready).
Notification titles are generic (not PR-specific) so they display
well as system notifications. The event payload includes prTitle,
repoOwner, repoName, baseBranch, headBranch so consumers can
format context-aware messages themselves.
In-app, the App Shell renders these events as PR toasts. Their
"View PR" action now navigates straight into the PR detail drawer
on /prs via buildPrsRouteSearch, with selectedPrId set to the
event's PR id and detailTab chosen from the event kind:
checks_failing → checks, changes_requested /
review_requested → activity, everything else → drawer overview.
This replaces the older "select lane + open lane inspector merge
tab" route, which depended on the lane being currently focused and
forced the user to leave the PRs surface to follow up on a PR
event.
The PR page no longer assumes every tab loads every workflow query:
- Queue state loads only for workflow-oriented tabs.
- Merge contexts load lazily per selected PR.
- Selected PR detail (status, checks, reviews, comments) loads on demand.
- Background refresh updates only the stale subset using fingerprints, not every PR on every cycle.
When GitHub reports a PR as not mergeable (typically branch
protection), ADE surfaces an explicit opt-in to attempt the merge
anyway. The detail pane shows a checkbox when the PR is open, has
no merge conflicts, but is flagged isMergeable: false. The merge
request still goes through GitHub's merge API — GitHub itself
decides whether the bypass is allowed.
After a successful GitHub merge, cleanup runs inside an outer try-catch so a cleanup failure does not mask the successful merge:
- branch deletion
- group membership removal
- lane archiving (if configured)
- base branch fetch
- cache invalidation
- rebase-needs scan
Individual failures log as warnings; the operation is marked
succeeded with a cleanupError metadata field when anything went
wrong.
prService.cleanupBranch is a second cleanup entry point scoped to the
PR branch itself rather than a lane. It is reachable from
PrLaneCleanupBanner when the PR is linked to the primary lane but its
head branch differs, which happens after a manual import / re-link.
Guarantees:
- refuses to run unless the PR is
mergedorclosed - refuses to delete any branch that matches a primary lane's branch ref
- local deletion uses
git branch -Daftergit show-ref --verify - remote deletion uses
git push <remote> --deleteaftergit ls-remote --headsconfirms the branch exists on the remote - returns a
CleanupPrBranchResultwith independentlocalDeleted/remoteDeletedbooleans and per-side error strings; partial failures logprs.branch_cleanup_partial_failurebut do not throw
linkToLane also now guards against cross-linking: linking a PR to a
lane whose branch ref does not match the PR's head branch throws
instead of silently linking mismatched branches.
ADE supports agent-driven resolution of PR issues for two scopes:
checks— after all checks have completed and at least one failedcomments— unresolved review threads (non-outdated)both— combined
prIssueResolver.ts assembles a structured prompt from live PR
state (failing checks + workflow run detail, unresolved threads with
compact summaries, changed files, recent commits) and launches a
chat agent session scoped to the lane worktree. The session gets
four workflow tools:
| Tool | Purpose |
|---|---|
prRefreshIssueInventory |
Re-pull checks / threads / comments |
prRerunFailedChecks |
Re-trigger failed GitHub Actions check runs |
prReplyToReviewThread |
Post a reply on a review thread |
prResolveReviewThread |
Mark a review thread resolved |
prRefreshIssueInventory evaluates checks with failure-first
priority: if any check has conclusion === "failure", the status is
"failing" regardless of other checks.
The generated prompt frames each session as one bounded Path-to-Merge round: the agent makes a coherent set of fixes for the current inventory, commits and pushes, and stops with a concise final note (what changed, what was validated, whether it pushed, and any blocker). The agent is explicitly told not to wait indefinitely for CI or advisory review bots — ADE's poller will observe post-push comments and launch the next round if new actionable work appears.
issueInventoryService.ts tracks PR issues (failing checks,
unresolved review threads, issue comments) in the pr_issue_inventory
table. It classifies by source (CodeRabbit, Codex, Copilot, human,
ADE), extracts severity from emoji/text patterns, and computes a
per-round ConvergenceStatus.
Thread tracking fields: thread_comment_count,
thread_latest_comment_id, thread_latest_comment_author,
thread_latest_comment_at, thread_latest_comment_source.
A thread is treated as fixed when GitHub reports it as resolved or
outdated, or when the latest reply on an unresolved thread from a
non-bot author pattern-matches as a resolution acknowledgement
(looksLikeResolutionAck in resolverUtils.ts). The helper rejects
obvious negations ("not fixed", "still not resolved", etc.) before it
accepts phrases like "fixed", "addressed", "no longer applies",
"clear-to-merge", or "CI green". Bot sources (CodeRabbit, Copilot,
Codex) still use the original resolved/outdated signal only.
Runtime state (pr_convergence_state table):
type ConvergenceRuntimeState = {
autoConvergeEnabled: boolean;
status: ConvergenceStatus; // idle, launching, running, polling, paused, converged, merged, failed, cancelled, stopped
pollerStatus: PollerStatus; // idle, scheduled, polling, waiting_for_checks, waiting_for_comments, paused, stopped
currentRound: number;
activeSessionId: string | null;
activeLaneId: string | null;
activeHref: string | null;
pauseReason: string | null;
errorMessage: string | null;
lastStartedAt, lastPolledAt, lastPausedAt, lastStoppedAt: string | null;
};PipelineSettings (per PR) drives both the manual auto-converge panel
and the Path-to-Merge orchestrator:
| Field | Purpose |
|---|---|
autoMerge |
When true, PtM lands the PR after convergence (or via the early-green gate). |
mergeMethod |
`repo_default |
maxRounds |
Hard cap on normal iterations before the loop either gives up or runs the force-finalize bonus iteration. Default 5. |
conflictStrategy |
`pause |
autoAgentSettings |
Provider / model / reasoning / permission mode / confidence threshold used when conflictStrategy === "auto" — also reused by the queue's standalone auto-resolve flow. |
forceFinalizeMode |
`off |
forceFinalizeRequireNoCiFailures |
When forceFinalizeMode === "conditional", the bonus iteration only fires if no required CI checks are failing. |
earlyMergeOnGreen |
Default true. Each iteration first checks whether checks are green and reviews are clean — if so, the merge ladder runs immediately instead of dispatching another fix round. |
onRebaseNeeded |
Legacy two-option projection (`pause |
The orchestrator persists per-PR start args (modelId, reasoning,
scope, additionalInstructions) in
pr_convergence_state.ptm_args_json so a desktop restart can rehydrate
the loop instead of pausing on missing model overrides.
The manual auto-converge poller (still used when PtM is not active)
waits for CI to finish and comments to stabilize (2 consecutive polls
with same count) before starting the next round. Auto-merge
additionally requires a non-empty check list: if GitHub returns zero
checks for the PR, the poller pauses with
Auto-merge paused because GitHub returned no check data for this PR.
instead of merging on vacuously-true "all checks passed".
Detail-pane inventory sync is now skipped entirely for merged or
closed PRs — syncInventory() returns early, refreshDetailSurface
omits the inventory leg, and PrConvergencePanel receives a
terminalState signal so the panel renders the terminal summary
instead of offering auto-converge controls. newIssueCount also zeroes
for terminal PRs so sticky action-bar badges don't attach to a dead PR.
An integration proposal can target an existing lane instead of always
creating a fresh integration-* child lane:
- The user selects a merge target lane in
IntegrationTaborCreatePrModal. The selected lane cannot be one of the proposal's source lanes and cannot be the primary lane. - Simulation persists
preferredIntegrationLaneIdplus the selected lane'smergeIntoHeadSha. This lets the UI warn when the adopted lane has drifted since the last preview. - Pairwise conflict checks between source lanes remain anchored to the
proposal's
baseBranch; additional merge-tree checks compare the adopted lane HEAD against each source lane so existing work on the target lane is represented. - Creating/committing the proposal either reuses the adopted lane
(
integrationLaneOrigin: "adopted") or creates an ADE-owned lane ("ade-created"). Cleanup messaging follows that origin: deleting a proposal keeps adopted lanes by default.
The corresponding database columns are
integration_proposals.preferred_integration_lane_id and
integration_proposals.merge_into_head_sha. iOS mirrors both in its
bootstrap schema and IntegrationProposal model so synced PR workflow
cards can display the same state.
PrDetailPane renders two different layouts for the Overview tab
depending on prsTimelineRailsEnabled in PrsContext:
- Legacy grid — the original checks/reviews/comments cards.
- Timeline + Rails —
PrDetailTimelineRailswith a central event timeline (PrTimeline), a commit rail, a status/deployments rail, and an AI summary card.
Per-PR state (all persisted to localStorage under
ade:prs:timelineFiltersByPrId, ade:prs:dismissedAiSummaries,
ade:prs:timelineRailsEnabled):
PrTimelineFilters— which event types to show (description, commits, reviews, threads, comments, checks, deployments, labels, merges).dismissedAiSummaries[prId]— whether the AI summary card is collapsed for this PR.viewerLogin— authenticated GitHub login used to highlight reactions the viewer already placed.
Deep linking: prsRouteState carries eventId, threadId,
commitSha, and detailTab in the URL. PRsPage preserves them as
long as the URL still points at the selected PR and drops them when the
PR changes. PrDetailPane reads them on mount to scroll / open the
right card and to pick the right sub-tab. PRsPage also writes the
most recent /prs... path to localStorage via writeStoredPrsRoute
scoped per project root, so the top-bar TabNav can route back to the
user's last PR selection when they click the PRs tab from elsewhere.
Commit sources: buildTimelineEvents folds in commits from two
streams — PrActivityEvent.commit_push entries and the
getCommits(prId) snapshot. Commits that appear in both are
deduplicated by SHA, with the activity path taking precedence (so
force-push metadata survives). Commit rows render as a full-width
"commit divider" instead of an inline timeline entry, so they visually
separate review / comment activity into before/after-commit bands.
Keyboard shortcuts (bound only when Timeline+Rails is active and the Overview tab is selected):
| Chord | Action |
|---|---|
g c |
Open the commit palette |
g t |
Open the unresolved-threads palette |
g f |
Open the changed-files palette |
[ / ] |
Prev / next unresolved thread |
prSummaryService generates a PrAiSummary (summary text, risk
areas, reviewer hotspots, unresolved concerns) via the AI integration
service and caches it in pull_request_ai_summaries keyed by
(pr_id, head_sha). Pushing new commits advances head_sha
(maintained by prService.upsertFromGithub) so the next read misses
and the summary regenerates. regenerateSummary forces a rebuild
regardless of cache state.
prPollingService writes last_polled_at on every PR after a
successful tick. The cursor is exposed via getLastPolledAt(prId) so
downstream services that hit GitHub with since= parameters (review
threads, comments) can skip work they already saw. The cursor is
best-effort — failures log a warning and do not abort the tick.
PRsPageparses URL state viaparsePrsRouteStateand writes it back withbuildPrsRouteSearch. Active tab, workflow sub-tab, selected PR, queue group, lane, and rebase item are all encoded.PrsContextmounts cheaply on the plain GitHub PR list. The initialrefreshCoreonly kicks a background GitHub refresh when the active tab is a workflow tab (queue/integration/rebase) or a PR is selected; otherwisegithubRefreshModeis left undefined so the renderer paints from the existing snapshot.applyLocalPrStatecallsprs.listWithConflicts({ includeConflictAnalysis: false })andlanes.list({ includeStatus: false })for the plain list, then enables conflict analysis, rebase-needs scans, and auto-rebase status reads only when a workflow tab or selected PR needs them.- Workflow surfaces batch PR merge context through
prs.getMergeContexts(prIds)instead of fanning out onegetMergeContext(prId)call per card. The service builds the batch from metadata-only lane rows so queue/integration/rebase views do not pay full git status cost on render. Conflict analysis also runs as one batch over metadata-only active lanes, preserving overlap warnings against non-PR peer lanes without per-PR conflict calls. PrsContextowns PR list, queue states, rebase needs, proposals, convergence runtime state, and the Timeline+Rails UI state (prsTimelineRailsEnabled,timelineFiltersByPrId,dismissedAiSummaries,viewerLogin,detailReviewThreads,detailDeployments,detailAiSummary). It caches convergence state per PR and exposesloadConvergenceState/saveConvergenceState/resetConvergenceState, plussetTimelineFilters,setAiSummaryDismissed, andregeneratePrAiSummary.PrDetailPaneis where most rich behavior concentrates: convergence panel (slide-over), issue resolver modal, rebase banner, check/review/comment sections with running indicators (PrCiRunningIndicator), merge readiness with bypass checkbox, PR markdown rendered withrehype-sanitizeafterrehype-raw.GitHubTabrenders the unified repo+external list; filter tab counts respect the active scope. Open views load open external PRs first; switching to Closed, Merged, or All asks the runtime for the closed external history snapshot.
The CTO agent has five dedicated tools for orchestrating convergence programmatically:
| Tool | Purpose |
|---|---|
getPullRequestConvergence |
Read runtime state + settings + inventory summary |
updatePullRequestConvergencePipeline |
Edit pipeline settings |
updatePullRequestConvergenceRuntime |
Edit runtime state |
startPullRequestConvergenceRound |
Launch the next convergence round |
stopPullRequestConvergence |
Stop the active run, interrupt chat session, persist stopped state |
The ADE CLI exposes the issue inventory service to terminal-capable agent workflows.
prService.getMobileSnapshot() produces a PrMobileSnapshot for the
iOS PRs tab in one call (exposed over sync as
prs.getMobileSnapshot). Types live in
apps/desktop/src/shared/types/prs.ts.
type PrMobileSnapshot = {
generatedAt: string;
prs: PrSummary[];
stacks: PrStackInfo[]; // lane chains with >=1 PR
capabilities: Record<string, PrActionCapabilities>; // per-PR action gates
createCapabilities: PrCreateCapabilities; // which lanes can create
workflowCards: PrWorkflowCard[]; // queue/integration/rebase
live: boolean; // false → phone banner
};Builder responsibilities:
- Stacks (
buildStackInfos/collectStackMembers) — walkslaneService.listin parent → child order, tagging each member withrole(root | middle | leaf),depth, and linked PR fields when a PR exists for the lane. Stacks without any PRs are dropped. - Capabilities (
capabilitiesForPr) — gatescanMergeonstate === "open"and non-failing checks; blocks merges on drafts and closed/merged PRs with an explicitmergeBlockedReason.requiresLiveis always true today — all listed actions need a live host. - Create eligibility (
buildCreateCapabilities) — enumerates non-primary, non-archived lanes, marks lanes as ineligible when an open/draft PR already exists, and resolves the default base branch throughresolveStableLaneBaseBranch. - Workflow cards (
buildWorkflowCards) — pulls non-terminal queue entries fromqueue_landing_statejoined withpr_groups, active integration proposals vialistIntegrationWorkflows({ view: "active" }), and rebase needs fromconflictService.scanRebaseNeeds()(filtered tokind === "lane_base"withbehindBy > 0and a live defer window). Using the same source the desktop Rebase tab consumes viawindow.ade.rebase.scanNeedskeeps the phone's rebase cards in sync with the desktop — including drift against a localmainthat hasn't been pushed yet, whichrebaseSuggestionServicemisses because it only readsorigin/<base>. Dismiss / defer rebase from the phone (lanes.dismissRebaseSuggestion,lanes.deferRebaseSuggestion) updatesconflictServicefirst so the next snapshot reflects the action immediately, then forwards torebaseSuggestionServicefor legacy parity. Failures in any source log a warning and skip that card category rather than failing the whole snapshot.
The snapshot is read-only; create/merge/close/comment actions go
through the existing command surface (prs.createFromLane,
prs.land, prs.close, prs.addComment, prs.rerunChecks,
prs.draftDescription). The mobile client calls getMobileSnapshot
on open and re-fetches on focus or after a successful mutation.
- Branch name validation in
CreatePrModalruns before submission and rejects invalid git ref characters. Skipping this produces opaque errors from the GitHub API. rehype-sanitizemust run afterrehype-rawin the PR body renderer. Flipping the order lets attacker-controlled HTML through.- Fingerprint exclusion list.
getPrFingerprintomits four fields. Adding a new volatile field without updating the exclusion list causes polling to emit notifications on every tick. - Queue transitions use
ALLOWED_TRANSITIONS. Invalid transitions are logged and rejected rather than silently applied. Cancel path force-fails entries in non-skippable states. - Post-merge cleanup is best-effort. Never wrap the merge itself in the same try-catch; the merge must be reported succeeded even if cleanup fails.
- Conflict marker parser handles CRLF.
parseConflictMarkersmatches both\nand\r\n. Windows checkouts depend on this. - Convergence auto-advance needs two stable comment polls. Shortening this to one causes the poller to race GitHub's comment propagation.
- Review thread resolution uses GraphQL.
prService's GraphQL path backsgetReviewThreads,replyToReviewThread, andresolveReviewThread. The REST API does not expose all the required fields.