Repository navigation
[Bug]: PR handoffs (Resolve/Fix/Checkout) check out into an unrelated project when the linked repository has no matching checkout #16455
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Have two GitHub-backed projects in one environment, e.g.
unrelated(added first,acme/unrelated) andapp(added later,acme/app). - In a thread of
app, link a pull request under a repository name that no local project matches. The way I hit it: the agent linkedold-org/app#123, then the repository was renamed/transferred toacme/appand the checkout's remote was updated. GitHub still redirects the old name. A thread linked to a PR in a repository you have no checkout of (frontend thread → backend PR) takes the same path. - Make that PR conflict with its base, open the thread details panel, and press Resolve on the PR row (Fix / Checkout from the PR panel behave the same).
Expected behavior
The handoff checks the PR out in the project that is actually a checkout of that repository, or, when there isn't one, refuses with a clear message ("No project here is a checkout of acme/app").
Actual behavior
The checkout and the new thread land in unrelated. The worktree is created under ~/.t3/worktrees/unrelated/t3code-pr-123-..., the PR head (and, for a fork PR, the fork's history) is fetched into unrelated's repository, its setup script runs, and the conflict-resolution prompt is put in a composer of the wrong project. The toast reports success, so nothing suggests it went wrong. The fetch into an unrelated repo also makes the handoff very slow.
Cause:
ThreadDetailsPrRowsends the stored link as the reference:{ projectId: app, host: "github.com", repository: "old-org/app", number }.requireProject(apps/server/src/pullRequest/PullRequestService.ts) finds the own project's repository doesn't match, no project on the host matches either, and falls back toonHost.find((candidate) => candidate.api.kind !== "azure-devops"), which is the first GitHub project in project order. That fallback exists so a thread can read a PR from another repository with any checkout's credentials, and the read succeeds (GitHub follows the rename).detailthen reports that borrowed project asprojectId/workspaceRoot, andusePullRequestHandoffs.startHandoff(apps/web/src/components/pullRequest/usePullRequestActions.ts) opens the thread and runspreparePullRequestThreadthere.GitManager.preparePullRequestThreaddoes not check that the PR belongs to the cwd's repository.
Repro at the service level: with projects [unrelated (acme/unrelated), app (acme/app)], service.detail({ projectId: "app", host: "github.com", repository: "old-org/app", number: 123 }) returns projectId: "unrelated", workspaceRoot: "/unrelated".
Suggested direction: keep the borrowed-credentials route for reads, but don't present the borrowed project as the PR's checkout. Handoffs should refuse (or pick the real checkout) when no project's repository matches. Optionally, refresh a link's stored repository from the canonical URL the host returns, so links survive renames/transfers.
Impact
Major degradation or frequent failure
Version or commit
main @ d60a71e
Environment
Linux (Fedora 44), desktop app, Claude provider
Workaround
Re-link the PR under its current repository name, or check the branch out manually in the right project.
Activity
juliusmarminge commentedon Oct 6, 2026
Note
Grok responding on behalf of Julius.
Triage
Thanks @Absterrg0 for the clear write-up. I read this against
main(9bd1d8009a, which contains the reportedd60a71ef6). The report matches the code, and I didn't find another issue for this handoff landing in the wrong project.What I found
requireProjectinapps/server/src/pullRequest/PullRequestService.tsprefers a checkout of that repository, then takes the oldest non-Azure project on the host (projection_projectsisORDER BY created_at ASC, project_id ASC). That donor project is how a read borrows another checkout's credentials, and the test "routes a hosted reference to another repository through a project on that host" covers the read. Azure already requires a matching repository and is left out of this fallback.workspaceRootand only rewritesrepositoryandremote, anddetailandsummarycopy those intoprojectIdandworkspaceRoot. Since the match is a string compare of the storedowner/repo, a link toold-org/appresolves to the oldest project even when another project is the checkout of the renamedacme/app. GitHub follows the rename, so the read itself succeeds.ThreadDetailsPrRow→usePullRequestHandoffs) opens the thread ondetail.projectIdand runsgit.preparePullRequestThreadwithcwdset todetail.workspaceRoot.PullRequestDetailPanelhas its own copy of the same handoff and uses the same values when opened from a thread. The success toast is the checkout toast, so a wrong project still looks successful.GitManager.preparePullRequestThreaddoesn't check that the cwd is a checkout of the pull request's repository. Worktree mode fetches the head throughmaterializePullRequestHeadBranch(ensureRemoteplus a fetch into that cwd), which is why the worktree shows up under the unrelated project. For comparison, a hostless reference naming another repository already fails with "The change request does not belong to the selected project."Likely fix area
Some options, which could keep reads working through another checkout's credentials while not using that donor as the checkout target:
detailandsummaryidentify only a real checkout of the repository, or leave the checkout target empty when there isn't one.preparePullRequestThreadthat refuses a cwd whose remote is a different repository.owner/repothe host returns, so a renamed or transferred repository matches the checkout that already has the new remote.Until then, re-linking under the current repository name, or checking the branch out in the right project manually, avoids it.
A maintainer will decide on the fix direction.
fix(pull-requests): check out a PR only in a checkout of its repository
test(server): pull request checkouts outside the repository's own che…