Skip to content

WorktreeMgr

Defined in: src/worktree.ts:48

new WorktreeMgr(): WorktreeMgr

WorktreeMgr

behindBase(worktreePath, baseBranch): Promise<boolean | null>

Defined in: src/worktree.ts:398

Whether the branch checked out at worktreePath is BEHIND baseBranch — i.e. base has commits not yet in HEAD, so a strict merge train must rebase first. Best-effort fetches origin/<base> and prefers it; falls back to the local base ref when offline. Returns: false → base is an ancestor of HEAD (up-to-date, safe to merge) true → base has commits HEAD lacks (stale, rebase needed) null → unknowable (bad worktree / git error) → caller treats as “do not merge”

string

string

Promise<boolean | null>


branchExists(repoPath, branch): boolean

Defined in: src/worktree.ts:180

True iff a local branch branch exists in repoPath. Used to pre-empt a rename collision before touching any remote.

string

string

boolean


commitsAhead(repoPath, baseBranch, branch): number

Defined in: src/worktree.ts:224

Commits on branch not yet on baseBranch (git rev-list --count base..branch). 0 means the branch tip still equals base — the “nothing committed yet” window in which an auto-rename can safely move the branch. Returns a large number on any git error so callers treat an unknowable state as “not safe to rename”.

string

string

string

number


containsCommit(worktreePath, sha): boolean | null

Defined in: src/worktree.ts:456

Whether sha is reachable from the branch tip checked out at worktreePath — i.e. the commit genuinely belongs to this session’s branch. Used to reject a PR that gh pr list --head <name> matched purely on branch NAME: a prior, already-merged PR that reused this branch name reports a head commit that this freshly-cut branch does not contain. Returns: true → sha is HEAD or an ancestor of it (this branch’s own commit) false → sha is absent locally OR not an ancestor (a foreign / stale PR) null → unknowable (worktree unusable / git couldn’t run) → caller keeps the PR

Two known limitations of the reachability test:

  • Assumes squash/rebase merges (this repo’s policy — merge commits are disabled). Under those, a merged feature commit is NOT an ancestor of main, so a reused-name branch cut from main fails to reach the old PR head → the collision is caught. On a repo using plain merge commits, the old PR’s head stays reachable from main, so a fresh same-name branch would still “own” it and the collision would slip through.
  • Transient false-negative after a self-rebase. If a session’s own PR merges and the still-active branch is then rebased/reset, the PR head is no longer an ancestor of HEAD, so a genuine MERGED is dropped to none. Rare (a session usually archives once its PR lands) and self-heals once the PR head returns to the history; acceptable versus the false-MERGED it prevents.

string

string

boolean | null


create(repoPath, baseBranch, name): WorktreeResult

Defined in: src/worktree.ts:58

string

string

string

WorktreeResult


createDetached(repoPath, branch, sha, slug?, pullRef?): Promise<WorktreeResult>

Defined in: src/worktree.ts:564

Detached worktree at a specific commit, fetching it from origin first so a PR head pushed by the agent is present even when the local repo is behind. Used by the critic to review the exact PR head.

slug disambiguates the worktree path when callers do NOT have a unique sha to key on. The PR critic detaches at the PR head sha (unique per PR), so it omits slug and the path stays …-review-<sha> — reused-on-restart to reclaim an interrupted run (the existsSync-reclaim below is load-bearing for that slugless path). The plan reviewer, however, detaches every session at the SAME base-branch sha, so without a slug all plan reviews in a repo would collide on one path: a second begin() would blow away the first’s live worktree, and both inflight records would then read the same .shepherd-plan-review.json — delivering one run’s plan findings to another. It passes a per-RUN unique id (the reviewer’s pinned session id, a fresh randomUUID per spawn) as slug, so the path disambiguates across RUNS — even two reviews of the SAME session at the SAME sha get distinct paths (#631), not just two different sessions.

pullRef is the OPTIONAL fork escape hatch for the standalone PR critic. A fork PR’s head sha is NOT on the base repo’s origin (it lives on the contributor’s fork), so the branch fetch below can’t land it and worktree add --detach <sha> would fail with a missing object. When provided (e.g. refs/pull/<n>/head, which GitHub exposes on the base repo’s origin) it is ALSO fetched — same ---guarded grammar as the branch fetch — so the head sha reaches the local store before the checkout. Same-repo callers (ReviewService, the plan reviewer) omit it and behavior is unchanged.

string

string

string

string

string

Promise<WorktreeResult>


currentBranch(worktreePath): string | null

Defined in: src/worktree.ts:207

The branch currently checked out in worktreePath, or null when HEAD is detached or the path isn’t a readable git worktree. This is the source of truth for which branch a session’s PR will come from — an agent that runs git checkout -b / git branch -m moves it out from under the stored value.

string

string | null


ensureBaseRef(repoPath, baseBranch): Promise<ResolvedBase>

Defined in: src/worktree.ts:264

Freshen baseBranch from upstream at task-launch time, then return a ResolvedBase the caller uses to base the new worktree on.

Contract:

  1. Validates baseBranch — fails closed (no git calls) on an invalid refname.
  2. Calls upstreamStatus() to fetch and evaluate the branch against origin.
  3. Computes baseRef:
    • Non-diverged branch with an upstream → upstream sha (the new worktree starts fresh even when the local fast-forward below is skipped).
    • Diverged or no upstream → branch name (fall back to whatever is local).
  4. Best-effort local fast-forward (never throws, never blocks):
    • No upstream → localFf = "none".
    • Diverged → warn, localFf = "skipped-diverged", local untouched.
    • Already up-to-date → localFf = "not-needed".
    • Behind / origin-only → determine if baseBranch is HEAD in repoPath:
      • Checked out here (clean tree) → git merge --ff-only <sha> → “applied”.
      • Checked out here (dirty tree) → skip, localFf = "skipped-dirty" (warn).
      • Not checked out here → git branch -f <branch> <sha> (creates or ff’s the local ref without touching any working tree). Fails when the branch is HEAD in ANOTHER worktree → localFf = "skipped-checked-out-elsewhere" (warn). baseRef is unaffected — the new task still starts at the upstream sha.

All git calls are async (no sync I/O on the server loop) and individually try/caught. ensureBaseRef never throws.

string

string

Promise<ResolvedBase>


gitCommonDir(worktreePath): string

Defined in: src/worktree.ts:488

The ABSOLUTE shared git object store for worktreePath (git rev-parse --git-common-dir, resolved against the worktree). A worktree’s own .git is a file pointing here; the bwrap membrane must bind this store rw so the agent’s git ops reach the real refs/objects. Falls back to <worktreePath>/.git on any git error so a non-worktree (isolated:false) still yields a usable path.

string

string


remove(worktreePath, opts?): void

Defined in: src/worktree.ts:501

string

string

string | null

void


renameBranch(repoPath, oldBranch, newBranch): void

Defined in: src/worktree.ts:196

Rename a local branch. git branch -m works even while the branch is checked out in a worktree, so this is safe to run on a live session’s branch. Throws when the target name is already taken (the caller surfaces that as a conflict).

string

string

string

void