# How Worktrees and Project Grouping Resolve in `lib/worktree.ts`

> Discover how Pi-Web's lib/worktree.ts resolves worktrees and project grouping through cached discovery, Git interrogation, and path derivation. Optimize your Git workflow today.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-08-18

---

**The [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) module in Pi Web resolves projects through three phases: cached discovery, Git interrogation via `git rev-parse`, and path‑based project shape derivation to correctly group sessions by repository and worktree.**

Pi Web is a terminal session manager that groups sessions by Git project and supports Git worktrees for parallel branch development. The [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) file contains the core logic that determines which project a session belongs to, whether that directory is a worktree, and how to enumerate or create worktrees. This article breaks down the resolution algorithm, caching strategy, and worktree utilities as implemented in the `agegr/pi-web` source code.

## Project Discovery and Caching in `resolveProject`

The entry point for all project resolution is `resolveProject`. This async function returns a `ProjectInfo` object containing `projectRoot`, `branch`, `isWorktree`, and `isTopLevel` flags.

### The One-Minute Cache

Results are cached on `globalThis.__piProjectCache` for up to 60 seconds to avoid repeated Git subprocess calls. The cache is created lazily on first access:

```typescript
// Lines 43-46 in lib/worktree.ts
const cache = (globalThis.__piProjectCache ??= new Map<string, CacheEntry>());
const cached = cache.get(cwd);
if (cached && Date.now() - cached.ts < 60_000) {
  return cached.info;
}

```

### Handling Deleted Worktrees

When a directory no longer exists on disk—common when a worktree was manually deleted—`inferRemovedWorktree` attempts to map it back to a deleted entry under `<repo>-worktrees`. This prevents the UI from displaying phantom projects:

```typescript
// Lines 77-83 in lib/worktree.ts
const inferred = await inferRemovedWorktree(cwd);
if (inferred) {
  return inferred; // Returns cached info with projectRoot rewired to main repo
}

```

## Git Interrogation with Deterministic Locale

All Git commands execute through a `git` helper that forces `LC_ALL=C` and applies a short timeout. This ensures error message matching works across different system locales:

```typescript
// Lines 52-60 in lib/worktree.ts
async function git(cwd: string, args: string[]): Promise<string> {
  const result = await $`git ${args}`.cwd(cwd).timeout("5s").env("LC_ALL", "C");
  return result.stdout.trim();
}

```

### The Four rev-parse Arguments

`resolveProject` calls `git rev-parse` with four arguments to gather repository metadata:

| Argument | Purpose |
|----------|---------|
| `--git-common-dir` | Shared `.git` directory for all linked worktrees |
| `--git-dir` | Actual `.git` directory of current worktree (differs for linked worktrees) |
| `--show-toplevel` | Repository top-level path |
| `--abbrev-ref HEAD` | Current branch name, or `HEAD` if detached |

These four values enable the module to distinguish between the main repository, linked worktrees, and subdirectories within either.

## Deriving the Project Shape

After normalizing paths with `toNativePath` and `realPathOrSelf` from [`lib/paths.ts`](https://github.com/agegr/pi-web/blob/main/lib/paths.ts), `resolveProject` computes three key properties:

**`isTopLevel`** – True when `realCwd` equals the `toplevel` path (compared via `samePath`).

**`isWorktreeTopLevel`** – True when `gitDir` ≠ `commonDir` *and* `isTopLevel` is true. This indicates the current directory is the root of a linked worktree, not the main repository.

**`projectRoot`** – The grouping key for sessions:
- For linked-worktree top levels: parent of the shared `.git` folder
- Otherwise: the `toplevel` path (or `cwd` itself for non-Git directories)

The final object is cached and returned:

```typescript
// Lines 126-128 in lib/worktree.ts
const info: ProjectInfo = { projectRoot, branch, isWorktree, isTopLevel };
cache.set(cwd, { info, ts: Date.now() });
return info;

```

## Worktree Enumeration and Management

Beyond project resolution, [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) provides utilities for creating, listing, and removing worktrees.

### `listWorktrees`: Parse Porcelain Output

Parses `git worktree list --porcelain` into structured objects, filtering prunable or missing entries:

```typescript
// Lines 44-78 in lib/worktree.ts
export async function listWorktrees(cwd: string): Promise<WorktreeInfo[]> {
  const lines = await git(cwd, ["worktree", "list", "--porcelain"]);
  // Parses worktree path, branch, HEAD, and prunable status
}

```

### `findCurrentWorktreePath`: Locate Containing Worktree

Given a list of worktrees and a `cwd`, finds which worktree contains the directory using real path comparison:

```typescript
// Lines 85-87 in lib/worktree.ts
export function findCurrentWorktreePath(worktrees: WorktreeInfo[], cwd: string): string | undefined {
  return worktrees.find(w => samePath(w.path, cwd))? w.path;
}

```

### `addWorktree`: Create Linked Worktrees

Creates a new worktree under `<repo>-worktrees/<sanitized-branch>`, registers the path as an allowed file root, and invalidates the project cache:

```typescript
// Lines 93-104 in lib/worktree.ts
export async function addWorktree(cwd: string, branch: string) {
  const sanitized = sanitizeBranchForDir(branch);
  const path = join(repoRoot, "..", `${basename(repoRoot)}-worktrees`, sanitized);
  await git(cwd, ["worktree", "add", path, branch]);
  await addAllowedRoot(path); // From lib/allowed-roots.ts
  invalidateProjectCache();
  return { path, branch: usedBranch };
}

```

### `removeWorktree`: Clean Removal

Calls `git worktree remove` with optional `--force` and clears the cache:

```typescript
// Lines 132-140 in lib/worktree.ts
export async function removeWorktree(cwd: string, path: string, force = false) {
  const args = ["worktree", "remove", ...(force ? ["--force"] : []), path];
  await git(cwd, args);
  removeAllowedRoot(path);
  invalidateProjectCache();
}

```

### `sanitizeBranchForDir`: Safe Directory Names

Converts branch names to filesystem-safe strings by replacing illegal characters with hyphens:

```typescript
// Lines 89-91 in lib/worktree.ts
export function sanitizeBranchForDir(branch: string): string {
  return branch.replace(/[\\/:*?"<>|]+/g, "-");
}

```

## Practical Usage Examples

### Resolve Project Information

```typescript
import { resolveProject } from "./lib/worktree";

async function showProjectInfo(cwd: string) {
  const info = await resolveProject(cwd);
  console.log(`Root: ${info.projectRoot}`);
  console.log(`Branch: ${info.branch ?? "detached"}`);
  console.log(`Worktree? ${info.isWorktree}`);
  console.log(`Top-level? ${info.isTopLevel}`);
}

```

### List and Select Current Worktree

```typescript
import { listWorktrees, findCurrentWorktreePath } from "./lib/worktree";

async function pickWorktree(cwd: string) {
  const worktrees = await listWorktrees(cwd);
  const current = findCurrentWorktreePath(worktrees, cwd);
  console.log("All worktrees:", worktrees);
  console.log("Current worktree path:", current);
}

```

### Create a Feature Branch Worktree

```typescript
import { addWorktree } from "./lib/worktree";

async function createFeatureWorktree(cwd: string, branch = "feature/foo") {
  const { path, branch: used } = await addWorktree(cwd, branch);
  console.log(`Created worktree at ${path} for branch ${used}`);
}

```

## UI Grouping Implications

According to the Pi Web source code, session grouping relies entirely on `resolveProject` output:

- Sessions are grouped by `projectRoot`
- Worktree sessions at their top-level (`isWorktree: true`, `isTopLevel: true`) appear as separate sidebar rows
- Subdirectories inside worktrees retain their own `projectRoot` equal to `cwd`, preventing accidental regrouping
- Deleted worktrees trigger `inferRemovedWorktree` to rewire the session back to the main repository

The [`app/api/sessions/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/sessions/route.ts) endpoint consumes `resolveProject` to attach sessions to the correct project, while [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts) ensures newly created worktrees are readable by the `/api/files` endpoint.

## Summary

- **`resolveProject`** in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) is the central function for project discovery, using a 60-second cache and `git rev-parse` interrogation
- **Project shape** is derived from four Git paths: `commonDir`, `gitDir`, `toplevel`, and branch name
- **`isWorktreeTopLevel`** distinguishes linked worktree roots from main repository directories
- **Worktree utilities** (`listWorktrees`, `addWorktree`, `removeWorktree`) provide full CRUD operations with automatic cache invalidation
- **Path normalization** via [`lib/paths.ts`](https://github.com/agegr/pi-web/blob/main/lib/paths.ts) utilities ensures cross-platform correctness

## Frequently Asked Questions

### How does Pi Web handle sessions in deleted worktrees?

When `resolveProject` encounters a non-existent directory, `inferRemovedWorktree` attempts to map it back to a deleted worktree entry under `<repo>-worktrees`. If found, it rewires the `projectRoot` to the main repository, preventing phantom projects from appearing in the sidebar.

### Why does the `git` helper force `LC_ALL=C`?

The helper at lines 52-60 sets `LC_ALL=C` to ensure Git error messages are in English. This allows reliable string matching on error conditions across different system locales, which is critical for correctly handling non-Git directories and deleted worktrees.

### What is the difference between `isWorktree` and `isWorktreeTopLevel`?

`isWorktree` indicates the directory belongs to a linked worktree (true whenever `gitDir` ≠ `commonDir`). `isWorktreeTopLevel` is stricter: it requires both the worktree condition AND `isTopLevel` to be true, meaning the current directory is specifically the root of that linked worktree.