Skip to content

PRs: keep merged pull requests after their lane is deleted - #988

Merged
arul28 merged 4 commits into
mainfrom
ade/pr-merged-lane-mapping
Aug 1, 2026
Merged

PRs: keep merged pull requests after their lane is deleted#988
arul28 merged 4 commits into
mainfrom
ade/pr-merged-lane-mapping

Conversation

@arul28

@arul28 arul28 commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Problem

In the PRs tab's Merged bucket, nearly every row showed an amber unmapped badge.

The cause wasn't cosmetic: deleting a lane hard-deleted its pull_requests row, so the normal "merge → delete the lane and branch" flow erased ADE's record that the PR was ever ADE's. Each row also silently lost its CI outcome, review result and diff stats (all derived from the linked row), and the detail pane fell back to a synthetic stub whose only affordance — "map to a lane" — could never fire on a merged PR.

So the merged tab was ADE's own shipped-work history, rendered as anonymous GitHub rows with a warning on each.

Approach

Lane deletion, branch switch and rename now soft-detach instead of deleting.

  • detached_at + the lane's name, colour, and a frozen count of its chats/artifacts/checkpoints. Those counts cannot be recomputed later — the lane's sessions and artifacts are hard-deleted with it, and there is no commits table — so they're captured before the cascade runs.
  • lane_id is deliberately left dangling. It is NOT NULL on a cr-sqlite CRR in PHONE_CRITICAL_CRR_TABLES, and cr-sqlite cannot alter nullability without a full table rebuild. CRR already strips FKs, so the dangling id is inert and doubles as a provenance key.
  • Storage goes down, not up. Detach nulls the bulky snapshot JSON (files/checks/comments/reviews) after lifting commit and file counts onto the row. pull_request_snapshots was 33% of a real 26 MB db.

detached_at is null gates every lane-scoped "what is this lane working on" read. Project-wide reads deliberately keep detached rows — that's how the merged view recovers its provenance. A detached row owns nothing, so it can't block re-mapping, and a lane reclaims one only when it still exists and still tracks the PR's head branch.

Also

  • Merge outcome recorded: who merged, by what method, commit/file counts — captured in land() and on the poller's merge transition, with no extra GitHub calls. Null for PRs merged before this shipped; the UI degrades rather than showing blanks.
  • Merged row re-cut: no mapping badge in terminal buckets, neutral-not-amber in Open unless actionable, was: <lane> provenance chip, merge facts replacing the branch pair, sticky day/week period headers with per-period totals.
  • Detail pane: mapping controls suppressed for terminal PRs; the existing merge rail gains a shipped summary. The two-click Delete branch button starts working for these PRs for free, since pr.laneId now survives.
  • Web is the same renderer — ships free; only fixtures added.
  • iOS mirrors all of it.

Pre-existing bug fixed in passing

iOS declared only 4 of pull_requests' columns. Desktop has written merge_conflicts and behind_base_by since the merge-conflict work (prService.ts:2446 UPDATE, :2506 INSERT), and the phone never declared either. An unknown column in a cr-sqlite changeset throws and nacks the whole batch, freezing replication for that device. Both are now mirrored alongside the 9 new ones.

Testing

  • Desktop: 1079 tests across prs/lanes/conflicts/review; shard 1/8 green (1345)
  • CLI/TUI: 1388 tests
  • iOS: BUILD SUCCEEDED; 1115 tests, no new failures (6 pre-existing, verified against a clean tree — one is flaky: 0/2/2 failures across three identical runs, nondeterministic JSON key ordering)
  • 5 new regression tests pin the three blockers /quality caught: duplicate-PK insert on detached rows, permanently-broken re-mapping, and a first fix whose gate was too loose

Not done

Database.swift's local-mirror SELECT/upsert still don't carry the new columns — hand-numbered positional indices, no coverage, and no benefit, since GitHubPrListItem decodes from the host RPC rather than the mirror. PullRequestListItem.detached stays nil on iOS and degrades cleanly.

Needs a real two-device check before merge: that a pull_requests changeset carrying the new columns applies cleanly desktop→phone.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Merged and closed pull requests are grouped by time period with counts and change summaries.
    • Pull-request details show merge author, method, timing, commit counts, and changed-file counts.
    • Historical lane information remains visible when a lane is deleted or changed.
    • iOS and desktop views provide consistent merge and lane-history details.
  • Bug Fixes

    • Detached pull requests no longer appear as active mappings or trigger unnecessary rebase actions.
    • Terminal pull requests no longer show irrelevant unmapped, review, or CI indicators.

Deleting a lane hard-deleted its `pull_requests` row, so the normal
"merge, then delete the lane and branch" flow erased ADE's record that the
PR was ever ADE's. The Merged bucket rendered almost entirely as amber
`unmapped` badges, and each row silently lost its CI outcome, review
result and diff stats along with the row.

Lane deletion, branch switch and rename now soft-detach instead: the row
survives with `detached_at` plus the lane's name, colour and a frozen
count of its chats, artifacts and checkpoints. Those counts cannot be
recomputed later — the lane's sessions and artifacts are deleted with it —
so they are captured before the cascade runs. `lane_id` is deliberately
left dangling: it is NOT NULL on a cr-sqlite CRR that the phone treats as
critical, and cr-sqlite cannot alter nullability without a table rebuild.

Detaching also nulls the bulky snapshot JSON after lifting commit and file
counts onto the row, so retaining history costs less storage than the
delete it replaces.

`detached_at is null` now gates every lane-scoped "what is this lane
working on" read; project-wide reads keep detached rows, which is how the
merged view recovers its provenance. A detached row owns nothing, so it
can never block re-mapping, and a lane reclaims one only when it still
exists and still tracks the PR's head branch.

Merged PRs also now record how they shipped — who merged, by what method,
commit and file counts — captured at merge and on the poller's transition,
with no extra GitHub calls.

The merged row is re-cut to match: no mapping badge in terminal buckets,
neutral rather than amber in Open unless the badge is actionable, a
`was: <lane>` provenance chip, merge facts in place of the branch pair,
and sticky day/week period headers with per-period totals. The PR detail
pane drops the mapping controls that could never fire on a merged PR and
gains a shipped summary in the merge rail.

iOS mirrors all of it, including two columns (`merge_conflicts`,
`behind_base_by`) that desktop already wrote but the phone never declared
— an unrelated pre-existing gap in the same block, where an unknown column
nacks the whole changeset and freezes replication.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Aug 1, 2026 1:00am

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@arul28, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbcea775-6e5d-41c9-851d-c2ede042710c

📥 Commits

Reviewing files that changed from the base of the PR and between 59668cc and 14a7e99.

📒 Files selected for processing (1)
  • apps/ios/ADETests/ADETests.swift
📝 Walkthrough

Walkthrough

The change adds soft-detached pull-request records with preserved lane provenance and merge metadata. Active lookups exclude detached rows. Desktop, iOS, and CLI views display terminal PR history and grouped results.

Changes

Detached pull-request history

Layer / File(s) Summary
Soft-detach storage and lane cleanup
apps/desktop/src/main/services/state/kvDb.ts, apps/desktop/src/main/services/prs/pullRequestRowCleanup.ts, apps/desktop/src/main/services/lanes/laneService.ts, apps/desktop/src/main/services/prs/pullRequestRowCleanup.test.ts, apps/desktop/src/main/services/lanes/laneService.test.ts
Lane deletion and branch changes preserve PR rows, record detachment metadata and provenance, clear bulky snapshot fields, and remove lane-owned memberships.
PR liveness, reclamation, and merge metadata
apps/desktop/src/main/services/prs/prService.ts, apps/desktop/src/main/services/prs/prService.test.ts, apps/desktop/src/main/services/conflicts/conflictService.ts, apps/desktop/src/main/services/lanes/autoRebaseService.ts, apps/desktop/src/main/services/lanes/rebaseSuggestionService.ts, apps/desktop/src/main/services/review/reviewContextBuilder.ts
Active queries exclude detached rows. Matching lanes can reclaim detached rows. Unmatched rows retain historical provenance and expose merge facts.
Desktop terminal PR history UI
apps/desktop/src/shared/types/prs.ts, apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx, apps/desktop/src/renderer/components/prs/shared/prListGrouping.ts, apps/desktop/src/renderer/components/prs/shared/PrDetailMergeRail.tsx, apps/desktop/src/renderer/components/prs/detail/PrDetailPane.tsx, apps/desktop/src/renderer/browserMock.ts, apps/desktop/src/renderer/components/prs/tabs/GitHubTab.test.tsx, apps/desktop/src/renderer/components/prs/shared/prListGrouping.test.ts, apps/desktop/src/renderer/components/prs/shared/PrDetailMergeRail.test.tsx
Merged and closed PRs use period groups, merge facts, diff statistics, detached lane history, and terminal-state mapping rules.
iOS terminal PR history UI
apps/ios/ADE/Models/RemoteModels.swift, apps/ios/ADE/Services/Database.swift, apps/ios/ADE/Views/PRs/*
iOS models and projections carry detached and merge metadata. Terminal PR rows and detail rails display that metadata, and merged or closed lists use period headers.
CLI detached lane display
apps/ade-cli/src/cli.ts, apps/ade-cli/src/tuiClient/rightPaneFormatters.ts, apps/ade-cli/src/tuiClient/__tests__/rightPaneFormatters.test.ts
Detached lane mappings display historical lane names or deleted lane instead of active lane identifiers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • arul28/ADE#240: Refines PR service lane-scoped lookup and handling of inactive PR rows.
  • arul28/ADE#322: Overlaps with lane deletion and PR-to-lane mapping behavior.
  • arul28/ADE#857: Overlaps with GitHub PR snapshot and projection handling.

Suggested labels: desktop, ios, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preserving merged pull requests after their ADE lane is deleted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/pr-merged-lane-mapping

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arul28 arul28 changed the title PR Merged Lane Mapping -> Primary PRs: keep merged pull requests after their lane is deleted Jul 31, 2026
@arul28 arul28 closed this Jul 31, 2026
@arul28 arul28 reopened this Jul 31, 2026
The pull_request event produced no GitHub Actions check suite for
d56fbb4 — railway, cursor, graphite, vercel, mintlify and greptile all
registered one, Actions alone did not, and close/reopen did not fire it
either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx (1)

1081-1096: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Period grouping assumes an order the callers do not guarantee. Both grouping helpers key a row on the merge timestamp, while both list callers sort by update time or by a user-selected sort option. When the two orders disagree, one period emits several headers and the period id repeats, which also breaks list-row identity.

  • apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx#L1081-L1096: sort filteredItems by prListGroupTimestamp when filter is merged or closed, and reuse that flag for buildPrListRows.
  • apps/desktop/src/renderer/components/prs/shared/prListGrouping.ts#L99-L130: keep the documented newest-first contract, and make the period id unique per occurrence so a repeated period cannot collide on the React key.
  • apps/ios/ADE/Views/PRs/PrsRootScreen.swift#L834-L851: sort the merged and closed repoItems by prListGroupDate before calling prListPeriodGroups, so ForEach receives unique PrListPeriodGroup ids.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx` around lines
1081 - 1096, Update apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx
lines 1081-1096 to reuse a merged/closed grouping flag, sort filteredItems by
prListGroupTimestamp for those filters, and pass the flag to buildPrListRows. In
apps/desktop/src/renderer/components/prs/shared/prListGrouping.ts lines 99-130,
preserve the newest-first contract and generate unique period ids for repeated
occurrences. In apps/ios/ADE/Views/PRs/PrsRootScreen.swift lines 834-851, sort
merged and closed repoItems by prListGroupDate before calling prListPeriodGroups
so ForEach receives unique group ids.
apps/desktop/src/main/services/lanes/laneService.ts (1)

4494-4517: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Exclude already-detached rows from the stale-PR lookup.

In switchBranch (Lines 4499-4507) and resolveBranchDrift (Lines 4693-4701), the stalePrRows query selects by lane_id, project_id, and head_branch <> targetBranchRef. It does not filter detached_at is null.

Before this change, a matched row was hard-deleted, so it could never reappear in a later switch. Now the matched rows are soft-detached and keep the same lane_id and the same (now permanently stale) head_branch, so every later branch switch on the same lane re-selects them again. Each re-selection reruns detachPullRequestRowsByIds: countLaneProvenance, the commit/file-count UPDATE, the snapshot-nulling UPDATE, and the pr_group_members DELETE all execute again against rows that are already historical. This is idempotent, so it is not a correctness bug, but the set of reprocessed rows grows without bound as a lane keeps switching branches.

Add and detached_at is null to both queries so they select only rows that still need to be detached.

⚡ Proposed fix for both occurrences
         const stalePrRows = db.all<{ id: string }>(
           `
             select id from pull_requests
             where lane_id = ?
               and project_id = ?
               and head_branch <> ?
+              and detached_at is null
           `,
           [row.id, projectId, targetBranchRef],
         );

Also applies to: 4688-4711

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/lanes/laneService.ts` around lines 4494 -
4517, Update the stalePrRows queries in both switchBranch and resolveBranchDrift
to add a detached_at is null predicate, so already-detached pull request rows
are excluded from subsequent stale-row processing while active stale rows
continue through detachPullRequestRowsByIds.
🧹 Nitpick comments (5)
apps/ios/ADE/Services/Database.swift (1)

3016-3021: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The new columns are mirrored but never read.

The schema now carries merged_by_login, merged_by_avatar_url, merge_method, commit_count, changed_files, and the detached_* columns. fetchPullRequestsLocked and fetchPullRequestListItemsLocked do not select them, and replacePullRequestHydrationLocked does not write them. So PullRequestListItem.mergedBy, mergeMethod, commitCount, changedFiles, and detached stay nil for a mapped PR. PrDetailScreen.shippedFacts then shows merge facts only when a GitHubPrListItem is present.

The PR description records this as follow-up work, so the mirroring here is the correct scope. Do you want me to extend the SELECT list, the row struct, and the upsert so the mapped row supplies these facts?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Services/Database.swift` around lines 3016 - 3021, The new
pull-request columns are added to the schema but are not persisted or loaded,
leaving mapped merge and detached facts nil. Extend fetchPullRequestsLocked and
fetchPullRequestListItemsLocked to select the mirrored fields, add them to the
corresponding row struct, and update replacePullRequestHydrationLocked to write
them while preserving existing GitHubPrListItem mappings.
apps/ios/ADE/Views/PRs/PrsRootScreen.swift (1)

842-842: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Memoize the period groups.

prListPeriodGroups(repoItems) runs on every body pass. This file already memoizes the filter, sort, and count derivations in githubDerived for that reason. Compute the groups in recomputeGitHubDerived and store them in state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/PRs/PrsRootScreen.swift` at line 842, Update the
GitHub-derived state flow by computing prListPeriodGroups(repoItems) inside
recomputeGitHubDerived and storing the result in githubDerived alongside the
existing filter, sort, and count values. Change the ForEach in PRsRootScreen
body to read the memoized groups from githubDerived instead of invoking
prListPeriodGroups(repoItems) during rendering.
apps/ios/ADE/Views/PRs/PrHelpers.swift (1)

1142-1142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the ISO8601DateFormatter and align formatter safety with the file's existing pattern.

Line 1142 allocates a new ISO8601DateFormatter for every grouped row. prListPeriodGroups calls this once per item, so a long merged history pays repeated allocation on the render path. Store one formatter next to prDayMonthFormatter.

This file already guards prIsoFormatter with prDateFormatterLock, because DateFormatter is not thread-safe. The new global formatters have no such guard. If prListGroupLabel can run off the main actor, use the same lock. Under Swift 6 strict concurrency, a global let of a non-Sendable class also needs nonisolated(unsafe) or an isolation annotation. Please confirm the package's Swift language mode.

♻️ Proposed change
-      let key = ISO8601DateFormatter().string(from: itemWeek.start).prefix(10)
+      let key = prWeekKeyFormatter.string(from: itemWeek.start).prefix(10)
let prWeekKeyFormatter = ISO8601DateFormatter()

Also applies to: 1187-1197

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/PRs/PrHelpers.swift` at line 1142, Hoist a shared
ISO8601DateFormatter beside prDayMonthFormatter and reuse it in
prListPeriodGroups and the related prListGroupLabel path instead of allocating
one per item. Align access with the existing prDateFormatterLock when these
helpers may run off the main actor, and apply the package’s Swift 6 concurrency
requirement (such as nonisolated(unsafe) or the established isolation
annotation) to the shared formatter.
apps/desktop/src/main/services/state/kvDb.ts (1)

2400-2421: 🗄️ Data Integrity & Integration | 🔵 Trivial

Confirm the iOS mirror schema is updated before this ships.

pull_requests is a CRR-replicated table. This migration adds nine new columns (detached_at, detached_lane_name, detached_lane_color, detached_provenance, merged_by_login, merged_by_avatar_url, merge_method, commit_count, changed_files) to it. An earlier comment in this same file, for terminal_sessions.settle_override, documents the established failure mode for this exact situation: a missing iOS half does not fail on desktop, it surfaces as changeset-apply errors on the phone. The PR objectives state that the iOS mirror's SELECT/upsert code does not yet include these new columns. Confirm that apps/ios/ADE/Resources/DatabaseBootstrap.sql and Database.swift's ensureColumn migrations add matching columns before this ships, and that apps/desktop/src/shared/types/prs.ts is updated to carry the new fields across the IPC boundary.

Separately, detached_at rows are never purged (per the PR design, they persist indefinitely to keep merged-PR history). Consider whether the existing idx_pull_requests_project_id index is sufficient for project-wide merged-view queries as this table grows, or whether a composite index on (project_id, detached_at) or (project_id, state) would help.

Based on the coding guideline "Keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes" and the PR objectives noting "Local iOS mirror SELECT/upsert code does not yet include the new columns."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/state/kvDb.ts` around lines 2400 - 2421,
Update the iOS mirror schema and synchronization paths for all nine new
pull_requests fields: add matching columns in DatabaseBootstrap.sql, include
them in Database.swift ensureColumn migrations and SELECT/upsert logic, and
extend apps/desktop/src/shared/types/prs.ts so the fields cross the IPC
boundary. Also assess the existing idx_pull_requests_project_id against detached
merged-view query patterns and add an appropriate composite index if needed.

Source: Coding guidelines

apps/desktop/src/main/services/prs/prService.test.ts (1)

6498-6652: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good regression coverage for the detach/reattach identity behavior.

The new suite exercises the three important cases: update-not-reinsert by identity, staying detached when the lane is gone or moved to another branch, and reclaiming a detached row when a lane returns to the same branch. This directly backs the upsertRow reattach logic.

None of these tests exercise linkToLane's manual re-link path, which is where the missing allowRepoPrAdoption gap was found (see the prService.ts comment on Line 6465). Consider adding a case that calls linkToLane against a detached row on the same branch to lock in the fix once applied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/prs/prService.test.ts` around lines 6498 -
6652, Add regression coverage for the manual relink path by calling linkToLane
with a detached PR row whose lane is on the same branch. Assert the operation
succeeds and preserves the expected adoption behavior controlled by
allowRepoPrAdoption, complementing the existing getStatus reattach tests. Use
the existing detachedRow, database setup, and service builders in the detached
PR suite.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/services/prs/prService.ts`:
- Around line 2512-2542: The reattachment path in upsertRow must not clear
detached provenance for terminal PRs selected by bestByLane. Require the
incoming summary state to be open or draft before treating an existing detached
row as reattachesToLiveLane, while preserving the existing lane and branch
checks for in-flight PRs.

In `@apps/ios/ADE/Views/PRs/PrRowCard.swift`:
- Around line 130-136: Update accessibilitySummary and the related live-state
text around the visible row summary logic to include branch-pair, CI, and
review-state details only when !data.isTerminal, while preserving provenance,
lane, merge, and cleanup text as appropriate. Update warnMessage so it returns
nil for terminal rows, preventing PrWarnBanner from displaying stale CI or
workflow data.

---

Outside diff comments:
In `@apps/desktop/src/main/services/lanes/laneService.ts`:
- Around line 4494-4517: Update the stalePrRows queries in both switchBranch and
resolveBranchDrift to add a detached_at is null predicate, so already-detached
pull request rows are excluded from subsequent stale-row processing while active
stale rows continue through detachPullRequestRowsByIds.

In `@apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx`:
- Around line 1081-1096: Update
apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx lines 1081-1096 to
reuse a merged/closed grouping flag, sort filteredItems by prListGroupTimestamp
for those filters, and pass the flag to buildPrListRows. In
apps/desktop/src/renderer/components/prs/shared/prListGrouping.ts lines 99-130,
preserve the newest-first contract and generate unique period ids for repeated
occurrences. In apps/ios/ADE/Views/PRs/PrsRootScreen.swift lines 834-851, sort
merged and closed repoItems by prListGroupDate before calling prListPeriodGroups
so ForEach receives unique group ids.

---

Nitpick comments:
In `@apps/desktop/src/main/services/prs/prService.test.ts`:
- Around line 6498-6652: Add regression coverage for the manual relink path by
calling linkToLane with a detached PR row whose lane is on the same branch.
Assert the operation succeeds and preserves the expected adoption behavior
controlled by allowRepoPrAdoption, complementing the existing getStatus reattach
tests. Use the existing detachedRow, database setup, and service builders in the
detached PR suite.

In `@apps/desktop/src/main/services/state/kvDb.ts`:
- Around line 2400-2421: Update the iOS mirror schema and synchronization paths
for all nine new pull_requests fields: add matching columns in
DatabaseBootstrap.sql, include them in Database.swift ensureColumn migrations
and SELECT/upsert logic, and extend apps/desktop/src/shared/types/prs.ts so the
fields cross the IPC boundary. Also assess the existing
idx_pull_requests_project_id against detached merged-view query patterns and add
an appropriate composite index if needed.

In `@apps/ios/ADE/Services/Database.swift`:
- Around line 3016-3021: The new pull-request columns are added to the schema
but are not persisted or loaded, leaving mapped merge and detached facts nil.
Extend fetchPullRequestsLocked and fetchPullRequestListItemsLocked to select the
mirrored fields, add them to the corresponding row struct, and update
replacePullRequestHydrationLocked to write them while preserving existing
GitHubPrListItem mappings.

In `@apps/ios/ADE/Views/PRs/PrHelpers.swift`:
- Line 1142: Hoist a shared ISO8601DateFormatter beside prDayMonthFormatter and
reuse it in prListPeriodGroups and the related prListGroupLabel path instead of
allocating one per item. Align access with the existing prDateFormatterLock when
these helpers may run off the main actor, and apply the package’s Swift 6
concurrency requirement (such as nonisolated(unsafe) or the established
isolation annotation) to the shared formatter.

In `@apps/ios/ADE/Views/PRs/PrsRootScreen.swift`:
- Line 842: Update the GitHub-derived state flow by computing
prListPeriodGroups(repoItems) inside recomputeGitHubDerived and storing the
result in githubDerived alongside the existing filter, sort, and count values.
Change the ForEach in PRsRootScreen body to read the memoized groups from
githubDerived instead of invoking prListPeriodGroups(repoItems) during
rendering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 03a0865c-b3e5-4670-9c3e-7fca156b2aed

📥 Commits

Reviewing files that changed from the base of the PR and between aa72548 and 7d0843b.

⛔ Files ignored due to path filters (3)
  • docs/features/lanes/README.md is excluded by !docs/**
  • docs/features/pull-requests/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
📒 Files selected for processing (32)
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/tuiClient/__tests__/rightPaneFormatters.test.ts
  • apps/ade-cli/src/tuiClient/rightPaneFormatters.ts
  • apps/desktop/src/main/services/conflicts/conflictService.ts
  • apps/desktop/src/main/services/lanes/autoRebaseService.ts
  • apps/desktop/src/main/services/lanes/laneService.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/lanes/rebaseSuggestionService.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/prs/pullRequestRowCleanup.test.ts
  • apps/desktop/src/main/services/prs/pullRequestRowCleanup.ts
  • apps/desktop/src/main/services/review/reviewContextBuilder.test.ts
  • apps/desktop/src/main/services/review/reviewContextBuilder.ts
  • apps/desktop/src/main/services/state/kvDb.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/prs/detail/PrDetailPane.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrDetailMergeRail.test.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrDetailMergeRail.tsx
  • apps/desktop/src/renderer/components/prs/shared/prListGrouping.test.ts
  • apps/desktop/src/renderer/components/prs/shared/prListGrouping.ts
  • apps/desktop/src/renderer/components/prs/tabs/GitHubTab.test.tsx
  • apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx
  • apps/desktop/src/shared/types/prs.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/Database.swift
  • apps/ios/ADE/Views/PRs/PrDetailOverviewTab.swift
  • apps/ios/ADE/Views/PRs/PrDetailScreen.swift
  • apps/ios/ADE/Views/PRs/PrHelpers.swift
  • apps/ios/ADE/Views/PRs/PrListRowModifier.swift
  • apps/ios/ADE/Views/PRs/PrRowCard.swift
  • apps/ios/ADE/Views/PRs/PrsRootScreen.swift

Comment on lines +2512 to +2542
// Identity first. The lane-branch lookup is deliberately live-only (it answers
// "what is this lane working on"), so on its own it would miss a detached row and
// send us down the insert path with a primary key that already exists.
const existing = getRowById(summary.id)
?? (options?.allowRepoPrAdoption
? getRowForLaneBranch(summary.laneId, summary.headBranch)
?? getRowForRepoPr(summary.repoOwner, summary.repoName, summary.githubPrNumber)
: getRowForLaneBranch(summary.laneId, summary.headBranch));
if (existing) {
// A detached row is only reclaimed by a lane that still exists AND still tracks
// the PR's head branch.
//
// Lane existence alone is not enough: `switchBranch`/`rename` detach rows while
// the lane lives on, so a background refresh would otherwise reattach a PR to a
// lane that has moved to a different branch — reinstating exactly the stale
// reference the old DELETE prevented, and destroying the provenance snapshot on
// the way. The branch check also settles archived lanes correctly: one that still
// tracks the branch may reclaim its PR, one that moved on may not.
const reattachesToLiveLane = Boolean(
existing.detached_at
&& (() => {
const lane = db.get<{ branch_ref: string | null }>(
"select branch_ref from lanes where id = ? and project_id = ?",
[summary.laneId, projectId],
);
if (!lane) return false;
const laneBranch = normalizeBranchName(branchNameFromRef(lane.branch_ref ?? ""));
const prBranch = normalizeBranchName(branchNameFromRef(summary.headBranch ?? ""));
return Boolean(laneBranch) && laneBranch === prBranch;
})(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect candidate selection in backfillLanePrRowsFromGithubPulls and confirm
# whether closed/merged PRs can win as "best" candidates for a lane's branch.
rg -n "bestByLane" -B5 -A40 apps/desktop/src/main/services/prs/prService.ts

Repository: arul28/ADE

Length of output: 3362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Outline prService relevant symbols =="
ast-grep outline apps/desktop/src/main/services/prs/prService.ts --match upsertRow --view expanded || true
ast-grep outline apps/desktop/src/main/services/prs/prService.ts --match getRowById --view expanded || true
ast-grep outline apps/desktop/src/main/services/prs/prService.ts --match getRowForRepoPr --view expanded || true

echo
echo "== Section around reattachment block =="
sed -n '2480,2635p' apps/desktop/src/main/services/prs/prService.ts | cat -n

echo
echo "== Section around upsertRow implementation =="
sed -n '2000,2250p' apps/desktop/src/main/services/prs/prService.ts | cat -n

Repository: arul28/ADE

Length of output: 19808


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate relevant declarations and query macros =="
rg -n "const getRowForRepoPr|function getRowForRepoPr|getRowForRepoPr|LIVE_PR_ROWS|isActivePrState|rowMatchesLaneBranchForDisplay|projectionMatchesLaneBranchForDisplay" apps/desktop/src/main/services/prs/prService.ts

echo
echo "== getRowForRepoPr definition and nearby rows =="
rg -n -B8 -A80 "const getRowForRepoPr|function getRowForRepoPr" apps/desktop/src/main/services/prs/prService.ts | cat -n

echo
echo "== Macros around LIVE_PR_ROWS and candidate sort helpers =="
rg -n -B8 -A25 "LIVE_PR_ROWS|isActivePrState|rowMatchesLaneBranchForDisplay|projectionMatchesLaneBranchForDisplay|compareLanePrDisplayCandidates" apps/desktop/src/main/services/prs/prService.ts | cat -n

Repository: arul28/ADE

Length of output: 27995


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Exact upsert update block tail =="
sed -n '2620,2665p' apps/desktop/src/main/services/prs/prService.ts | cat -n

echo
echo "== Read-only deterministic model of reattachment conditions from source =="
python3 - <<'PY'
from pathlib import Path
import re

src = Path("apps/desktop/src/main/services/prs/prService.ts").read_text()

m_best = re.search(
    r"const\s+bestByLane\s*=\s*new\s+Map<\w+>\s*\(\)\s*;\s*"
    r"for\s*\(\s*const\s+rawPr\s+of\s+rawPulls\s*\)\s*\{"
    r"(?P<body>.*?)for\s*\(\s*const\s+\{ rawPr[\s\S]+?\n\s*\}\s+of\s+bestByLane\.values\(\)\s*\)",
    src,
)
if m_best:
    print("bestByLane_candidate_order_found")
    body = m_best.group("body")
    print("open_draft_can_be_preferred", 'isOpen && !prev.isOpen' in body)
    print("newest_can_be_preferred", 'isOpen === prev.isOpen && prNumber > prev.prNumber' in body)
    print("no_state_guard_in_bestByLane_loop", '"merged"' not in body and '"closed"' not in body and 'state === "open"' not in body)
else:
    print("bestByLane_candidate_order_not_found")

m_reattach = re.search(
    r"const\s+reattachesToLiveLane\s*=.*?\(\)&&(\s*;\s*|\))",
    src,
)
cond = m_reattach.group(0) if m_reattach else ""
print("reattachesToLiveLane_checks_branch_match", 'laneBranch === prBranch' in cond)
print("reattachesToLiveLane_checks_summary_state", bool(re.search(r"summary\.state|summary\.state\s*===|isOpen|draft|closed|merged", m_reattach.group(0) if m_reattach else "", re.S)))
PY

Repository: arul28/ADE

Length of output: 1793


Require an in-flight PR state before clearing detached provenance.

bestByLane selects closed/merged PRs when no open/draft PR exists on a branch, and upsertRow(..., { allowRepoPrAdoption: true }) adopts that row if the branch matches. A reused branch name can therefore clear detached_at, detached_lane_name, and detached_provenance for a detached merged/closed PR. Limit reclamation to incoming open/draft PRs, or add an explicit guard that does not clear provenance for terminal PR states.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/prs/prService.ts` around lines 2512 - 2542,
The reattachment path in upsertRow must not clear detached provenance for
terminal PRs selected by bestByLane. Require the incoming summary state to be
open or draft before treating an existing detached row as reattachesToLiveLane,
while preserving the existing lane and branch checks for in-flight PRs.

Comment on lines +130 to +136
} else if let provenance = data.provenanceLabel {
parts.append(provenance)
} else if let lane = data.laneLabel {
parts.append("Lane \(lane)")
}
if let facts = data.mergeFacts { parts.append("Merged \(facts)") }
if data.needsBranchCleanup { parts.append("Remote branch still exists") }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Suppress stale live-state text for terminal rows.

A merged or closed row can still announce its branch pair, CI state, and review state in accessibilitySummary. It can also render PrWarnBanner with stale CI or workflow data because warnMessage does not exclude terminal.

Gate these live-work signals with !data.isTerminal. Set warnMessage to nil when terminal is true.

Proposed fix
-    if let head = data.headBranch, let base = data.baseBranch { parts.append("\(head) into \(base)") }
-    if let ci = data.ciIndicator { parts.append(ci.title) }
-    if let review = data.reviewIndicator { parts.append(review.label) }
+    if !data.isTerminal, let head = data.headBranch, let base = data.baseBranch {
+      parts.append("\(head) into \(base)")
+    }
+    if !data.isTerminal, let ci = data.ciIndicator { parts.append(ci.title) }
+    if !data.isTerminal, let review = data.reviewIndicator { parts.append(review.label) }
...
-      self.warnMessage = unmapped
+      self.warnMessage = unmapped || terminal
         ? nil

Also applies to: 454-497

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/PRs/PrRowCard.swift` around lines 130 - 136, Update
accessibilitySummary and the related live-state text around the visible row
summary logic to include branch-pair, CI, and review-state details only when
!data.isTerminal, while preserving provenance, lane, merge, and cleanup text as
appropriate. Update warnMessage so it returns nil for terminal rows, preventing
PrWarnBanner from displaying stale CI or workflow data.

arul28 and others added 2 commits July 31, 2026 20:54
…pping

# Conflicts:
#	docs/features/pull-requests/README.md
An unknown column in a cr-sqlite changeset raises, nacks the whole batch,
and stalls replication for that device until an app update ships — so the
column mirroring is a sync liveness property, not a feature detail.

Covers both a current install and, more importantly, a phone that
installed before these columns existed: legacy schema, real bootstrap
migration on relaunch, then a desktop changeset carrying all 11 new
columns. Includes merge_conflicts and behind_base_by, which desktop has
written for some time without the phone declaring them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@arul28
arul28 merged commit 1a50e20 into main Aug 1, 2026
3 checks passed
@arul28
arul28 deleted the ade/pr-merged-lane-mapping branch August 1, 2026 01:10
arul28 added a commit that referenced this pull request Aug 1, 2026
* refactor(prs): apply the quality findings, and make /quality require it

The four findings from the PR #988 review were verified as real and then
left in the code. Applying them:

- The two `deriveGithubSnapshot*` mappers capture nothing from the
  prService factory — they are pure row->DTO functions and now live at
  module scope beside `rowToSummary`, where they can be tested without
  constructing a service.
- The sticky period header is derived during render from the range being
  drawn. It previously round-tripped through a ref and a state-setting
  effect, re-rendering the whole list every scroll frame to compute
  something already in hand.
- `detachRows` takes an id list instead of a SQL predicate string plus a
  params array that four separate statements had to agree with
  positionally.
- The GitHub PR row renderer moves to its own module. None of it depends
  on GitHubTab's state, so the tab is left as a coordinator: 2468 -> 1960
  lines.

The skill changes matter more than the refactor. /quality's synthesis
step said "apply the safe fixes ... gate the rest", which reads as
licence to verify a finding and then defer it — so a run could surface
real problems, fix none of them, and still report success. It now says
fix every verified finding at any severity, and narrows the gate to the
only two things that genuinely need the author: a product decision, or a
behavior change the branch was not asked to make. "Structural", "large"
and "out of scope" are named as non-reasons.

/ship gained the matching precondition: a non-empty quality gate blocks
the merge, rather than being mentioned afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(prs): close quality follow-up gaps

* ship: iteration 1 — resolve all quality findings

* ship: iteration 2 — address final review findings

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 1, 2026
ADE rendered "CI passed · 3 jobs" for PR #988, where zero CI jobs ran.
Three third-party apps reported `success` — CodeRabbit while rate-limited,
Vercel while cancelled by an ignored-build step, and a comment bot — and
GitHub Actions registered no check suite at all. The rollup asked "did
anything succeed?" rather than "was this code verified?", so absence
rendered as success. That is the failure mode a CI indicator exists to
prevent: strictly worse than showing nothing, because it asserts a fact
that is false.

The derivation moves to shared/prChecksRollup.ts, with three rules:

1. State mapping delegates to prPipelineState, so `skipped`/`neutral` can
   no longer masquerade as success and the rollup can no longer disagree
   with the per-job rows rendered beneath it.
2. A green requires a CI producer — GitHub Actions or a legacy commit
   status (Buildkite/CircleCI/Jenkins). Preview, review and comment apps
   still render as rows but cannot carry a green on their own.
3. Required contexts that never reported hold the rollup back. Sourced in
   three tiers because credentials differ in what GitHub will show them:
   `/rules/branches/{branch}` (rulesets, read access only, works for all
   five credential sources), classic branch protection (admin-only), and
   `mergeStateStatus === blocked` as corroboration. Unreadable means
   unknown, never "none required".

Both ingestion paths inherit this: ingestGithubWebhook uses its payload
only to resolve which PR changed, then re-derives through computeStatus.

New `not_run` state, distinct from `none`: `none` is a repo with no CI and
stays quiet, `not_run` means something was expected and nothing verified
the commit. It renders as a hollow dashed ring — an empty slot, not an
alarm — on desktop, iOS, the Work-chat card, Lanes, the graph, and the TUI.

The sweep found four more places `not_run` would have gone green:

* adeRpcServer summarizePrChecks initialised `overall = "passing"`, so
  zero checks reported passing. The same bug, in the CLI's own rollup.
* ChatPrPane returned an emerald "3/3 checks" from row counts alone.
* ChatGitToolbar bucketed `skipped` into the passed counter.
* getPrEdgeColor painted a not_run PR green whenever it had an approval.

Display-only by design. No merge button, action or automation is gated —
including iOS's merge gate, where only the "All checks green" sentence
changes and the tone that feeds canMerge is deliberately left alone.

Regression tests at the prChecksRollup layer so every surface inherits
them, using the real #988 payload as a fixture.

Fixes ADE-135

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 2, 2026
ADE rendered "CI passed · 3 jobs" for PR #988, where zero CI jobs ran.
Three third-party apps reported `success` — CodeRabbit while rate-limited,
Vercel while cancelled by an ignored-build step, and a comment bot — and
GitHub Actions registered no check suite at all. The rollup asked "did
anything succeed?" rather than "was this code verified?", so absence
rendered as success. That is the failure mode a CI indicator exists to
prevent: strictly worse than showing nothing, because it asserts a fact
that is false.

The derivation moves to shared/prChecksRollup.ts, with three rules:

1. State mapping delegates to prPipelineState, so `skipped`/`neutral` can
   no longer masquerade as success and the rollup can no longer disagree
   with the per-job rows rendered beneath it.
2. A green requires a CI producer — GitHub Actions or a legacy commit
   status (Buildkite/CircleCI/Jenkins). Preview, review and comment apps
   still render as rows but cannot carry a green on their own.
3. Required contexts that never reported hold the rollup back. Sourced in
   three tiers because credentials differ in what GitHub will show them:
   `/rules/branches/{branch}` (rulesets, read access only, works for all
   five credential sources), classic branch protection (admin-only), and
   `mergeStateStatus === blocked` as corroboration. Unreadable means
   unknown, never "none required".

Both ingestion paths inherit this: ingestGithubWebhook uses its payload
only to resolve which PR changed, then re-derives through computeStatus.

New `not_run` state, distinct from `none`: `none` is a repo with no CI and
stays quiet, `not_run` means something was expected and nothing verified
the commit. It renders as a hollow dashed ring — an empty slot, not an
alarm — on desktop, iOS, the Work-chat card, Lanes, the graph, and the TUI.

The sweep found four more places `not_run` would have gone green:

* adeRpcServer summarizePrChecks initialised `overall = "passing"`, so
  zero checks reported passing. The same bug, in the CLI's own rollup.
* ChatPrPane returned an emerald "3/3 checks" from row counts alone.
* ChatGitToolbar bucketed `skipped` into the passed counter.
* getPrEdgeColor painted a not_run PR green whenever it had an approval.

Display-only by design. No merge button, action or automation is gated —
including iOS's merge gate, where only the "All checks green" sentence
changes and the tone that feeds canMerge is deliberately left alone.

Regression tests at the prChecksRollup layer so every surface inherits
them, using the real #988 payload as a fixture.

Fixes ADE-135

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 2, 2026
Follow-up work on the same ticket, from /quality (36 findings), /test
(parity + regression coverage), and the ship-loop revalidation.

The original fix corrected the rollup but left the same disease on other
surfaces. Fourteen more places rendered a pass from producer-blind row
counts; each is now gated on the canonical rollup:

* the merge checklist, desktop and iOS, which said "All 3 checks passed"
  for PR #988's exact payload — the ticket's own bug on the surface a
  reader trusts most;
* adeRpcServer.summarizePrChecks, which initialised its verdict to
  "passing" so *zero* checks reported green, on the surface an agent
  reads before deciding to merge;
* PrChecksCard's header, PrDetailPane's pill, PrChecksTab's strip, the
  ade code drawer and right pane, ChatPrPane, ChatGitToolbar,
  rightPaneFormatters, LaneNode, getPrEdgeColor, iOS's stat strip and
  group cards, and PrLaneSummary.

Two design corrections to the original fix:

* The CI-producer rule was an Actions-only allowlist. CircleCI,
  Buildkite and Azure Pipelines report through the Checks API with their
  own app slugs, so that rule marked every such repo permanently
  unverified — the same bug pointing the other way. It is now a denylist
  of known non-CI apps, plus a branch-protection override: if every
  required context reported and passed, the commit was verified whoever
  ran it. The override is ordered after the in-flight check so it cannot
  claim a pass while a job is still running.
* The grace-window clock read PR `updated_at`, which GitHub bumps on
  every comment — including the comments posted by the very bots whose
  presence is the finding. It now reads the earliest check `started_at`,
  which a comment cannot move.

Also: a failed /check-runs fetch preserves the previous rollup instead of
persisting a false not_run (bestEffort returns [] on a 403, which is
indistinguishable from "no checks"); check runs carrying a terminal
conclusion before their status flips no longer stick at pending forever;
and pr_get_checks returns the persisted verdict rather than a row tally
that cannot see required contexts.

Tests: regression coverage at the prChecksRollup and requiredChecks
layers so every surface inherits it, plus named tests for the merge
checklist, the agent-facing RPC, the TUI's bare-array path, the
fetch-failure preservation, and lane summaries. The real #988 payload is
committed as a fixture.

Docs, iOS, the ade CLI and the TUI are updated in lockstep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 2, 2026
CodeRabbit review on #1004. Eleven findings; ten applied, one refuted.

The substantive one: a `not_run` rollup can coexist with pending or
failing THIRD-PARTY rows, and several surfaces evaluated those raw counts
first. A commit nothing verified would report "2 pending checks" or a
failing count instead of the honest answer — the same producer-blind
claim in a different tense. The canonical verdict now takes precedence
in the merge checklist, the merge blockers, and the ade code right pane.

Also:
* A stale commit whose CI was entirely skipped, with a required context
  that never reported, stayed `pending` forever: the guard admitted any
  producer rather than an actual pass.
* `fetchCombinedStatus` sat unwrapped in the same Promise.all as the
  guarded check-runs fetch, so a 403 there still aborted the whole
  refresh instead of preserving the last-known rollup.
* A supplied canonical `checksStatus` is authoritative in the TUI
  formatter; the row fallback speaks only when no verdict was sent.
* iOS bootstrap SQL declares the two ADE-135 columns, so a fresh install
  has them before cr-sqlite delivers a changeset carrying them.
* Fixture and naming corrections: browserMock rows carry real app slugs
  (an absent slug is CI-eligible by design, which silently defeated the
  fixture), the denylist test is named for the rule it asserts, the lane
  summary test is named for the path it actually exercises, and the iOS
  #988 preview clone no longer points at #559's URL.

Refuted: the mock row store's positional read was reported as broken by
the new upsert params. It targets a different UPDATE (the projection
write, unchanged here) and was already inert for the other one. Its
matcher is now specific so the two cannot be confused again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 2, 2026
…#1004)

* fix(prs): stop rendering "CI passed" when nothing verified the commit

ADE rendered "CI passed · 3 jobs" for PR #988, where zero CI jobs ran.
Three third-party apps reported `success` — CodeRabbit while rate-limited,
Vercel while cancelled by an ignored-build step, and a comment bot — and
GitHub Actions registered no check suite at all. The rollup asked "did
anything succeed?" rather than "was this code verified?", so absence
rendered as success. That is the failure mode a CI indicator exists to
prevent: strictly worse than showing nothing, because it asserts a fact
that is false.

The derivation moves to shared/prChecksRollup.ts, with three rules:

1. State mapping delegates to prPipelineState, so `skipped`/`neutral` can
   no longer masquerade as success and the rollup can no longer disagree
   with the per-job rows rendered beneath it.
2. A green requires a CI producer — GitHub Actions or a legacy commit
   status (Buildkite/CircleCI/Jenkins). Preview, review and comment apps
   still render as rows but cannot carry a green on their own.
3. Required contexts that never reported hold the rollup back. Sourced in
   three tiers because credentials differ in what GitHub will show them:
   `/rules/branches/{branch}` (rulesets, read access only, works for all
   five credential sources), classic branch protection (admin-only), and
   `mergeStateStatus === blocked` as corroboration. Unreadable means
   unknown, never "none required".

Both ingestion paths inherit this: ingestGithubWebhook uses its payload
only to resolve which PR changed, then re-derives through computeStatus.

New `not_run` state, distinct from `none`: `none` is a repo with no CI and
stays quiet, `not_run` means something was expected and nothing verified
the commit. It renders as a hollow dashed ring — an empty slot, not an
alarm — on desktop, iOS, the Work-chat card, Lanes, the graph, and the TUI.

The sweep found four more places `not_run` would have gone green:

* adeRpcServer summarizePrChecks initialised `overall = "passing"`, so
  zero checks reported passing. The same bug, in the CLI's own rollup.
* ChatPrPane returned an emerald "3/3 checks" from row counts alone.
* ChatGitToolbar bucketed `skipped` into the passed counter.
* getPrEdgeColor painted a not_run PR green whenever it had an approval.

Display-only by design. No merge button, action or automation is gated —
including iOS's merge gate, where only the "All checks green" sentence
changes and the tone that feeds canMerge is deliberately left alone.

Regression tests at the prChecksRollup layer so every surface inherits
them, using the real #988 payload as a fixture.

Fixes ADE-135

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(prs): harden the checks rollup across every surface, and prove it

Follow-up work on the same ticket, from /quality (36 findings), /test
(parity + regression coverage), and the ship-loop revalidation.

The original fix corrected the rollup but left the same disease on other
surfaces. Fourteen more places rendered a pass from producer-blind row
counts; each is now gated on the canonical rollup:

* the merge checklist, desktop and iOS, which said "All 3 checks passed"
  for PR #988's exact payload — the ticket's own bug on the surface a
  reader trusts most;
* adeRpcServer.summarizePrChecks, which initialised its verdict to
  "passing" so *zero* checks reported green, on the surface an agent
  reads before deciding to merge;
* PrChecksCard's header, PrDetailPane's pill, PrChecksTab's strip, the
  ade code drawer and right pane, ChatPrPane, ChatGitToolbar,
  rightPaneFormatters, LaneNode, getPrEdgeColor, iOS's stat strip and
  group cards, and PrLaneSummary.

Two design corrections to the original fix:

* The CI-producer rule was an Actions-only allowlist. CircleCI,
  Buildkite and Azure Pipelines report through the Checks API with their
  own app slugs, so that rule marked every such repo permanently
  unverified — the same bug pointing the other way. It is now a denylist
  of known non-CI apps, plus a branch-protection override: if every
  required context reported and passed, the commit was verified whoever
  ran it. The override is ordered after the in-flight check so it cannot
  claim a pass while a job is still running.
* The grace-window clock read PR `updated_at`, which GitHub bumps on
  every comment — including the comments posted by the very bots whose
  presence is the finding. It now reads the earliest check `started_at`,
  which a comment cannot move.

Also: a failed /check-runs fetch preserves the previous rollup instead of
persisting a false not_run (bestEffort returns [] on a 403, which is
indistinguishable from "no checks"); check runs carrying a terminal
conclusion before their status flips no longer stick at pending forever;
and pr_get_checks returns the persisted verdict rather than a row tally
that cannot see required contexts.

Tests: regression coverage at the prChecksRollup and requiredChecks
layers so every surface inherits it, plus named tests for the merge
checklist, the agent-facing RPC, the TUI's bare-array path, the
fetch-failure preservation, and lane summaries. The real #988 payload is
committed as a fixture.

Docs, iOS, the ade CLI and the TUI are updated in lockstep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(prs): address review — canonical rollup outranks raw check rows

CodeRabbit review on #1004. Eleven findings; ten applied, one refuted.

The substantive one: a `not_run` rollup can coexist with pending or
failing THIRD-PARTY rows, and several surfaces evaluated those raw counts
first. A commit nothing verified would report "2 pending checks" or a
failing count instead of the honest answer — the same producer-blind
claim in a different tense. The canonical verdict now takes precedence
in the merge checklist, the merge blockers, and the ade code right pane.

Also:
* A stale commit whose CI was entirely skipped, with a required context
  that never reported, stayed `pending` forever: the guard admitted any
  producer rather than an actual pass.
* `fetchCombinedStatus` sat unwrapped in the same Promise.all as the
  guarded check-runs fetch, so a 403 there still aborted the whole
  refresh instead of preserving the last-known rollup.
* A supplied canonical `checksStatus` is authoritative in the TUI
  formatter; the row fallback speaks only when no verdict was sent.
* iOS bootstrap SQL declares the two ADE-135 columns, so a fresh install
  has them before cr-sqlite delivers a changeset carrying them.
* Fixture and naming corrections: browserMock rows carry real app slugs
  (an absent slug is CI-eligible by design, which silently defeated the
  fixture), the denylist test is named for the rule it asserts, the lane
  summary test is named for the path it actually exercises, and the iOS
  #988 preview clone no longer points at #559's URL.

Refuted: the mock row store's positional read was reported as broken by
the new upsert params. It targets a different UPDATE (the projection
write, unchanged here) and was already inert for the other one. Its
matcher is now specific so the two cannot be confused again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant