Skip to content

doctor: external-fork PRs land in the main worktree instead of an isolated PR worktree #3358

@rjwalters

Description

@rjwalters

Problem

When /loom:doctor (or a loom-doctor subagent) runs on an external-fork PR — i.e., a PR whose branch name doesn't include a Loom issue number — it has no obvious worktree path to use, so it falls through to gh pr checkout <N> in whatever directory the agent was invoked from. In practice that's the orchestrator's main worktree, which means:

  1. The orchestrator's HEAD switches from main to the PR's branch
  2. Any agent that subsequently tries to run from the same shell sees the wrong git state
  3. Untracked files from the PR branch can be left behind when switching back to main (in the case I just hit, an orphaned src-tauri/gen/ directory left over from a pre-feat!: remove Tauri desktop app surface — CLI + daemon only #3353 branch)

Concrete incident (2026-05-28)

I had /loom:doctor take over jperla's external PR #3348 (jperla/loom:fix/claude-code-2.1-compat). The doctor ran gh pr checkout 3348 in the orchestrator's main repo worktree. After the doctor finished, the orchestrator was on branch fix/claude-code-2.1-compat instead of main, and src-tauri/gen/ was sitting untracked in the working tree (left over from a state when src-tauri/ existed before #3353 removed it).

Recovery was manual: git checkout main && git pull --ff-only + a rm -rf src-tauri/ cleanup. If the orchestrator had committed work in the wrong branch state between the doctor's exit and the manual recovery, that would have ended up on jperla's branch by accident.

For internal-Loom PRs this isn't a problem because the doctor uses .loom/worktrees/issue-N/ derived from the issue number in the branch name (feature/issue-N). External PRs have no such marker.

Proposal

When the doctor (or any subagent that needs to mutate a PR branch) operates on an external-fork PR:

  1. Create a dedicated worktree at .loom/worktrees/pr-<N>/ (a different path than issue worktrees so the pr- prefix is unambiguous).
  2. cd into that worktree before any git/gh pr checkout mutation.
  3. Push to the external fork's branch from inside the dedicated worktree.
  4. The worktree gets cleaned up at the end of the doctor pass (and on PR merge by merge-pr.sh).

The branch-name heuristic to distinguish issue PRs from external-fork PRs:

  • Branch matches feature/issue-<N> → use existing issue-worktree path .loom/worktrees/issue-<N>/
  • Anything else (external fork, ad-hoc branch name, etc.) → use .loom/worktrees/pr-<PR_NUMBER>/

The merge-pr.sh script already prints a warning when it can't derive an issue number from the branch:

Could not determine issue number from branch 'fix/claude-code-2.1-compat' for worktree cleanup

That warning would become a positive — merge-pr.sh and the doctor would cooperate on the pr-<N> path.

Acceptance

  • loom-doctor subagent / defaults/.claude/commands/loom/doctor.md updated to:
    • Detect whether the PR's branch matches feature/issue-<N> or is external/ad-hoc
    • Create .loom/worktrees/pr-<PR_NUMBER>/ (with .loom-managed sentinel) when the branch doesn't fit the issue pattern
    • cd into that worktree before any mutation
  • merge-pr.sh extended to clean up .loom/worktrees/pr-<N>/ on merge (in addition to existing .loom/worktrees/issue-<N>/ cleanup)
  • Worktree-creation helper (.loom/scripts/worktree.sh) accepts an explicit pr-<N> mode or is bypassed in favor of git worktree add directly (decision in scope of implementation)
  • Regression test: simulate a doctor pass on a PR with branch fix/foo-bar; verify the orchestrator's CWD HEAD is unchanged after the doctor returns

Out of scope

  • Auto-detecting that a PR is external vs internal by headRepositoryOwner rather than branch name. The branch-name heuristic is sufficient — internal PRs always use feature/issue-N, and external PRs can be anything.
  • Backfilling pr- worktree paths for existing in-flight PRs. Apply only to new doctor passes.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    loom:buildingBuilder is implementing this issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions