Skip to content

feat: open GitHub PRs as worktree candidates #33

Description

@andrewchng

PRD: Open GitHub PRs as worktree candidates

Status: Specced — backlog (build later)
Owner: Sessionizer plugin
Related: sessionizer.worktree-open, worktree candidate picker (src/worktree/candidates.ts, src/worktree/flow.ts)

Summary

Extend the existing worktree candidate picker so open GitHub pull requests (including cross-fork heads) appear alongside workspaces, checkouts, local branches, and remote branches. Selecting an open PR materializes a local branch named pr-{n} and opens it as a Herdr worktree workspace (with normal create-time layout bootstrap). Selecting an already-materialized pr-{n} reopens as-is.

Today remote branches only come from refs/remotes/*. Fork PR heads never appear after a normal git fetch origin, so contributor PRs cannot be opened through Sessionizer without manual gh pr checkout / fetch.

Goals

  • Browse open PRs for the selected base repo and open one as a worktree workspace.
  • Support cross-fork PR heads without adding fork remotes.
  • Keep a 1:1 identity: PR #n ↔ local branch pr-{n} (no duplicate “local + PR” rows for the same checkout).
  • Keep gh a soft dependency: missing/unauthenticated gh → no PR rows; git-only flow unchanged.
  • Reuse the existing create/open/bootstrap path (herdr worktree create / open, layout on new worktrees only).

Non-goals (v1)

  • Auto-refresh / reset pr-{n} to the latest PR tip on reopen.
  • Browsing closed/merged PRs (old checkouts remain via local/workspace pr-{n}).
  • Hard dependency on gh.
  • Separate plugin action (e.g. sessionizer.worktree-pr).
  • Config toggle or cache for gh pr list.
  • Non-interactive CLI flag (--pr 29).
  • Fancy PR preview (gh pr view body/diff) — keep existing candidate preview style.
  • Deduping same-repo remote head rows vs PR rows (both may appear).
  • Extra git config / branch metadata beyond the pr-{n} name.
  • Pre-fetch of all remotes on every picker open.

User experience

Triggering

Unchanged entry: sessionizer.worktree-open → project/repo picker → candidate picker.

Candidate list order

  1. Existing worktree workspaces
  2. Existing git worktree checkouts
  3. Open PRs (only those not already represented by pr-{n})
  4. Local branches
  5. Remote branches

Open PR row label

open pr  #<n>  <title>  <owner>:<headRefName>

Example:

open pr  #29  fix(worktree): gate branch fallback…  pperanich:fix/branch-exists-check

Detail/preview can include PR number, intended local branch pr-{n}, and fetch base (pull/{n}/head). Existing metadata/README-style preview is enough for v1.

After materialization (1:1)

State Picker shows
Never opened One open pr row for #n
Opened once One existing workspace / checkout / local row for branch pr-{n}
Open PR row Hidden once pr-{n} is in the seen-branch set

Selecting the existing row reopens as-is (focus/open path; no layout re-bootstrap; no auto-update to latest tip).

Same-repo PRs vs remotes

A same-repo PR may appear both as:

  • remote branch origin/<head> → would create local <head>
  • open pr #n … → creates local pr-{n}

Both are allowed in v1 (different identities).

Esc / new branch

Unchanged: Esc or empty candidate list still prompts for a new branch name.

Behavior detail

Listing PRs

When building candidates for a project:

  1. Run existing git/Herdr discovery as today.

  2. If gh is available and succeeds, run approximately:

    gh pr list --state open --limit 30 --json number,title,headRefName,headRepositoryOwner,author,isDraft,isCrossRepository

    (cwd / -R = selected project repo.) Include drafts. Default limit ~30.

  3. If gh is missing, non-zero, or unauthenticated: soft silent skip — no PR rows, no hard failure.

  4. For each open PR, if local/workspace/checkout already has branch pr-{n}, omit the PR row.

  5. Otherwise append a pull-request candidate.

First open (materialize)

On select of an open PR #n when pr-{n} does not yet exist:

  1. From the base repo root:

    git fetch origin pull/{n}/head:refs/heads/pr-{n}

    Equivalent is fine (e.g. fetch to a temp ref then create with --base), as long as the local branch ends up named pr-{n} at the PR head tip.

  2. Call existing create path:

    herdr worktree create --branch pr-{n} [--base <ref if needed>] …
    

    then normal bootstrap (layout) for new worktrees.

  3. Prefer not using gh pr checkout (avoids fighting branch naming and dirtying the main checkout).

gh is used for list only; checkout is git + herdr.

Reopen

If pr-{n} already exists as workspace, checkout, or local branch: existing open/focus/create-recovery paths apply. Do not fetch/reset to latest PR tip in v1.

Failure modes

Failure Behavior
gh list fails Skip PR section silently
git fetch pull/n/head fails Surface error; do not create worktree
Create/open herdr errors Existing worktree error/recovery behavior

Implementation sketch

New candidate kind

| {
    id: string; // e.g. pr:29
    kind: "pull-request";
    label: string;
    branch: string; // pr-{n}
    prNumber: number;
    title: string;
    headRefName: string;
    headOwner: string;
    previewPath: string; // project root
  }

Discovery

  • Extend discoverWorktreeCandidates / buildWorktreeCandidates (or merge after build) with optional listOpenPullRequests(project).
  • Injectable runtime seam (like git runtime) for tests — no live gh in unit tests.
  • seenBranches includes pr-{n} so PR rows dedupe once materialized.

Flow

  • intentFromCandidate for pull-request → ensure fetch + create-branch with branch: pr-{n} and appropriate base (or create branch via fetch refspec then open/create).
  • Preserve product rules: existing workspaces reopen as-is; only new worktrees get layout bootstrap (CONTEXT.md / ADR-0001).

Docs

  • README worktree selection table: add row for Open PR → fetch head + create pr-{n} worktree.
  • Note soft gh dependency for PR rows only.

Acceptance criteria

  • With gh authenticated, open PRs (including drafts and cross-fork) appear in the worktree candidate list after repo select.
  • Without gh, worktree flow behaves as today (no crash, no PR rows).
  • Selecting an open PR creates local branch pr-{n}, opens a worktree workspace, and applies layout only on first create.
  • Opening the picker again shows a single existing row for pr-{n}, not both PR and local.
  • Re-selecting that row reopens as-is (no tip refresh, no re-layout).
  • Candidate order: workspaces → checkouts → PRs → local → remote.
  • Open PR labels include number, title, and owner:head.
  • Unit tests cover candidate merge/dedupe and intent mapping with injected PR list / fetch seams.
  • README documents the PR candidate behavior and soft gh dependency.

Test plan

  • Injected listOpenPullRequests: empty, several open, drafts included, pr-{n} already in seenBranches → PR omitted.
  • intentFromCandidate / flow: first open calls fetch then create with pr-{n}; second path focuses existing workspace.
  • Soft skip when PR list runtime throws / returns unavailable.
  • Manual: open a cross-fork PR on a real repo; confirm worktree path and layout; reopen; confirm no duplicate PR row.

Design decisions log (from grill)

Decision Choice
Job Browse open PRs → open as worktree
UX placement Same candidate picker
gh dependency Soft silent skip
Local branch name pr-{n} only
1:1 / no duplicate rows Hide PR once pr-{n} exists
List network Always gh pr list when available
Open network Fetch that PR ref only on first select
PR set Open + drafts, limit ~30
Reopen As-is, no auto-update
List order After checkouts, before local/remote
Same-repo remote + PR Allow both
Materialize git fetch pull/n/head + herdr create
Label open pr #n title owner:head

Follow-ups (later)

  • Refresh pr-{n} to latest PR tip when clean / explicit action.
  • Cache or debounce gh pr list.
  • --pr <n> non-interactive entry.
  • Richer PR preview.
  • Optional hide of same-repo remote heads that match an open PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions