Skip to content

feat(scm): detect cross-fork PRs by scanning all project remotes#2368

Open
harshitsinghbhandari wants to merge 2 commits into
AgentWrapper:mainfrom
harshitsinghbhandari:ao/agent-orchestrator-25/upstream-pr-detect
Open

feat(scm): detect cross-fork PRs by scanning all project remotes#2368
harshitsinghbhandari wants to merge 2 commits into
AgentWrapper:mainfrom
harshitsinghbhandari:ao/agent-orchestrator-25/upstream-pr-detect

Conversation

@harshitsinghbhandari

Copy link
Copy Markdown
Collaborator

Problem

When a worker session opens a PR from the fork against an upstream base repo (the standard fork -> upstream contribution flow), the session never linked to that PR on the dashboard.

Root cause: the SCM observer only polled a project's push origin for open PRs. GitHub lists a PR under its base repo, not its head, so an origin-only scan can't see a PR whose base is upstream. Concretely, session agent-orchestrator-23 opened a PR with head harshitsinghbhandari/agent-orchestrator and base AgentWrapper/agent-orchestrator; the observer polled only the fork, the PR was never discovered, and no pr row was written, so the dashboard PR badge stayed empty.

Fix

Scan every GitHub remote in the project checkout (origin + upstreams / mirrors), read from git remote at the project path, and attribute a PR to a session only when its head branch lives in that session's push origin.

  • discoverSubjects now builds one scan target per (session x remote repo); resolveScanRepos reads all remotes and dedupes them against origin.
  • Attribution moves from a fixed "head == this repo" guard to candidatesForHeadRepo: a PR is claimed only when pr.HeadRepo equals a candidate session's push origin and the branch prefix matches. Same-repo PRs (head == origin == scanned repo) and cross-fork PRs (head == origin, scanned repo == upstream base) both attribute; a stranger's fork PR has a head repo no session owns and is dropped, preserving the existing no-misattribution guarantee.
  • Tracked cross-fork PRs are keyed for refresh by their own recorded repo (subjectRepoForPR uses pr.Repo, the upstream base) so the GraphQL refetch targets the base repo instead of the fork where the PR number doesn't exist.

Cost

One extra cheap RepoPRListGuard ETag check per additional remote per 30s tick (304s when nothing changed). Negligible for the 2-3 remotes AO projects typically carry.

Known ceiling

Remotes are read once per project per process; a remote added after the daemon started is picked up on restart.

Tests

  • TestPoll_DiscoversCrossForkPRFromUpstreamRemote: fork head + upstream base is discovered, attributed to the session, persisted with the upstream base repo, and refreshed against upstream.
  • TestPoll_IgnoresUpstreamPRFromForeignHead: a matching-branch PR on a scanned upstream whose head lives in a third-party fork is never fetched or persisted.

go build ./..., go vet ./internal/observe/..., and the observe/scm, adapters/scm, service/session suites (181 tests) all pass.

🤖 Generated with Claude Code

harshitsinghbhandari and others added 2 commits July 3, 2026 05:27
The SCM observer only polled a project's push origin for open PRs, so a
PR opened from the fork against an upstream base repo (the standard
fork -> upstream flow) was never discovered: GitHub lists a PR under its
base repo, not its head, so an origin-only scan can't see it. Sessions
that opened such PRs showed no PR on the dashboard.

Scan every GitHub remote in the project checkout (origin + upstreams /
mirrors) for open PRs, and attribute a PR to a session only when its
head branch lives in that session's push origin. This surfaces
cross-fork PRs while preserving the no-misattribution guarantee: a
stranger's fork PR has a head repo no session owns and is dropped.

Tracked cross-fork PRs are keyed for refresh by their own recorded repo
(the upstream base) so the GraphQL refetch targets the right repo.

Co-Authored-By: Harshit Singh Bhandari <dev@theharshitsingh.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