# How Pi-Web Resolves Project Roots with Git Worktrees: Inside the `resolveProject` Algorithm

> Discover how Pi-Web resolves project roots with Git worktrees. Learn about the `resolveProject` algorithm, its use of `git rev-parse`, and TTL caching for efficient Git operations.

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

---

**Pi-Web determines the project root by executing `git rev-parse` to locate the common `.git` directory, distinguishes linked worktrees from the main checkout by comparing `gitDir` and `commonDir`, and employs a 60-second TTL cache to optimize session grouping and file-system access control.**

The `agegr/pi-web` repository implements sophisticated Git worktree detection to maintain stable project identities across multiple checkouts. Understanding **pi-web project root resolution worktrees** logic is essential for developers extending the session management or file explorer features. The core implementation resides in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts), specifically within the `resolveProject` function that powers both the session loader and the worktree API.

## The Core Resolution Algorithm in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts)

The `resolveProject(cwd)` function serves as the single source of truth for determining whether a directory represents a main repository, a linked worktree, or a standalone project.

### Caching Strategy and Deleted Directory Handling

Before invoking Git, the function checks `globalThis.__piProjectCache` to avoid redundant operations. If the `cwd` no longer exists on disk, the system falls back to `inferRemovedWorktree`, which recognizes paths ending with `-worktrees` and reconstructs the main repository root:

```typescript
if (!parent.endsWith("-worktrees")) return null;
const repoRoot = parent.slice(0, -"-worktrees".length);
return { projectRoot: repoRoot, branch: basename(cwd), isWorktree: true, isTopLevel: true };

```

### Git Query and Path Normalization

For existing directories, `resolveProject` executes a single Git command that returns four critical values:

```typescript
git(cwd, [
  "rev-parse",
  "--path-format=absolute",
  "--show-toplevel",
  "--show-gitdir",
  "--git-common-dir",
  "HEAD"
])

```

The results provide:

- **commonDir**: The shared `.git` directory for all worktrees
- **gitDir**: The specific `.git` file or directory for this checkout
- **toplevel**: The repository's top-level working directory
- **ref**: The current branch or `HEAD` if detached

After resolving symlinks with `realpathSync(cwd)`, the function determines if the current directory is the top-level via `const isTopLevel = toplevel === realCwd`.

### Worktree Detection Logic

A linked worktree is identified when `gitDir !== commonDir` and `isTopLevel` is true. This condition indicates a separate working tree sharing the main repository's Git database. The final `ProjectInfo` object construction sets:

- `projectRoot`: `dirname(commonDir)` for linked worktrees, otherwise the `cwd` itself
- `branch`: The current branch name (omitted if detached HEAD)
- `isWorktree`: Boolean flag for UI rendering
- `isTopLevel`: Boolean indicating root directory status

## Handling Removed Worktrees

When users delete a worktree directory without removing it from Git's registry, `inferRemovedWorktree` provides graceful degradation. By convention, Pi-Web creates worktrees under `<repoRoot>-worktrees/<branch>`. The fallback logic strips the `-worktrees` suffix to identify the main project root, allowing the session loader to correctly group orphaned sessions under their parent repository.

## Integration Points: Sessions and API

The resolution data propagates through two primary integration points that enable Pi-Web's collaborative features.

### Session Loading and Grouping

In [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts), each session's `cwd` is resolved once and cached. The resulting `projectRoot` attaches to `SessionInfo`, while `worktreeBranch` populates only for linked worktrees:

```typescript
const project = s.cwd ? projectByCwd.get(s.cwd) : undefined;
return {
  projectRoot: project?.projectRoot ?? s.cwd,
  ...(project?.isWorktree && project.branch ? { worktreeBranch: project.branch } : {})
};

```

This enables the sidebar to visually group sessions by shared repository, regardless of whether they originate from the main checkout or a linked worktree.

### The Worktree API Endpoint

The `/api/worktrees` route utilizes `resolveProject` to authorize file system access. After establishing the project root, it lists available worktrees and registers each path with `allowFileRoot()`:

```typescript
const project = await resolveProject(cwd);
const worktrees = await listWorktrees(existsSync(cwd) ? cwd : project.projectRoot);
for (const w of worktrees) allowFileRoot(w.path);

```

This ensures the file explorer can browse worktree contents while maintaining security boundaries.

## Supporting Functions and Cache Management

Beyond resolution, [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) exports helper functions for worktree manipulation:

- **`listWorktrees(cwd)`**: Parses `git worktree list --porcelain` into structured objects, filtering prunable entries
- **`addWorktree(cwd, branch)`**: Creates new worktrees under the `-worktrees/` directory and invalidates the cache
- **`removeWorktree(cwd, path, force)`**: Executes `git worktree remove` and clears `globalThis.__piProjectCache`

The global cache operates with a 60-second TTL (`PROJECT_CACHE_TTL_MS`), balancing performance with freshness. Mutating operations explicitly call `invalidateProjectCache()` to ensure immediate consistency.

## Summary

- **Pi-Web project root resolution** relies on `git rev-parse` to locate the common Git directory and distinguish linked worktrees via directory comparison.
- The `resolveProject` function in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) implements a 60-second TTL cache via `globalThis.__piProjectCache` to optimize repeated lookups.
- **Worktree detection** occurs when `gitDir !== commonDir`, triggering special handling for project root assignment and branch metadata.
- **Removed worktrees** are inferred using the `-worktrees` path suffix convention, maintaining session grouping integrity even after directory deletion.
- Session loading ([`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts)) and the worktree API ([`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts)) consume this data to enable repository-scoped grouping and secure file access.

## Frequently Asked Questions

### How does pi-web handle sessions opened in deleted worktree directories?

When a worktree directory is deleted but the session persists, `resolveProject` detects the non-existent path and invokes `inferRemovedWorktree`. This function recognizes the `-worktrees` suffix in the parent directory, returns the main repository as the `projectRoot`, and preserves the branch name extracted from the deleted directory's basename. Sessions then group correctly under the main repository in the UI.

### What Git command does pi-web use to detect worktrees?

The system executes `git rev-parse --path-format=absolute --show-toplevel --show-gitdir --git-common-dir HEAD` in a single invocation. By comparing the `gitDir` (specific to this checkout) against the `commonDir` (shared across all worktrees), the code determines whether the current directory is a linked worktree.

### Why does pi-web cache project resolution results?

Performance optimization drives the caching strategy. The `globalThis.__piProjectCache` stores `ProjectInfo` objects with a 60-second TTL to avoid repeated Git process spawns when loading multiple sessions from the same directory. Cache invalidation occurs immediately after `addWorktree` or `removeWorktree` operations to ensure data consistency.

### Where does pi-web create new worktrees when using the API?

The `addWorktree` function creates worktrees under a sibling directory named `<repoRoot>-worktrees/<sanitized-branch-name>`. This convention enables the `inferRemovedWorktree` fallback to reconstruct the main repository path even when worktree directories are deleted outside the application.