Worktree & Multi-Agent Cache Management#

pw_ghish: GitHub CLI-like interface for Gerrit code reviews and CI checks

When you work with multiple AI coding agents or juggle several Gerrit changes in parallel, managing Git checkouts in a large Bazel repository quickly becomes tedious:

  • Running multiple agents in a single checkout clobbers your Git index, staged files, and working tree.

  • Creating traditional git worktree directories at arbitrary paths is slow and triggers a cold Bazel build from scratch for every new worktree, consuming hundreds of gigabytes of disk space.

  • Manually creating, configuring, and cleaning up IDE workspaces for each task adds friction to every context switch.

./gh wt automates Git worktree and Bazel cache management for AI agents and humans. You can spin up isolated project directories in seconds, run fast incremental builds backed by a shared pool of warm build slots, and synchronize workspaces automatically with your IDE.

        flowchart LR
    subgraph ActiveProjects ["Logical Projects (~/wrk/projects/)"]
        P1["rpc-buffer-fix"]
        P2["sensor-driver"]
    end

    subgraph SlotPool ["Warm Physical Slots (~/wrk/slots/)"]
        S1["pw-01 (Warm Bazel Server)"]
        S2["pw-02 (Warm Bazel Server)"]
        S3["pw-03 (Available)"]
    end

    subgraph SharedCache ["Shared Bazel Caches (~/.cache/)"]
        RC["Repository Cache (Hardlinks)"]
        DC["80 GB Auto-GC Disk Cache"]
    end

    subgraph ParkedProjects ["Parked Projects (0 Disk Slots)"]
        PP1["bazel-cleanup (Git Branch + Gerrit CL)"]
    end

    P1 -->|symlink| S1
    P2 -->|symlink| S2
    S1 --> SharedCache
    S2 --> SharedCache
    S3 --> SharedCache
    PP1 -.->|swap in on demand| S3
    

Setup & Quickstart Tutorial#

This tutorial walks through setting up your worktree pool and running concurrent projects with AI agents.

1. Initialize Your Environment#

Run ./gh wt init once from your primary repository checkout to create your pool of warm worktree slots, install the Gerrit commit-msg hook, and configure your shared Bazel cache snippet:

$ ./gh wt init
Inspecting & Converging gh-ish Worktree Environment...
======================================================

[✓] Git Primary Repo:        /home/user/wrk/pigweed
[✓] Gerrit commit-msg Hook:  Checked in primary repository
[+] Worktree Slot Pool:      Created 10/10 slots in /home/user/wrk/slots
[✓] Project Symlinks Dir:    /home/user/wrk/projects
[✓] Antigravity UI Sync:     Ready (using default v2 permission schema template)
[+] Bazel Config Snippet:    Active at ~/.config/pw_ghish/bazelrc.worktrees
[+] User ~/.bazelrc Hook:    Added try-import to ~/.bazelrc
[✓] Bazel Output Bases:      0 orphaned output bases found

Environment is healthy and ready!

2. Spin Up Your First Project#

Use ./gh wt use <project> to start a new task:

$ ./gh wt use rpc-buffer-fix
✓ Project "rpc-buffer-fix" mounted in slot pw-01 (branch: rpc-buffer-fix)
  Directory: /home/user/wrk/projects/rpc-buffer-fix

This command:

  1. Allocates warm physical slot pw-01 and checks out branch rpc-buffer-fix tracking origin/main.

  2. Creates the symbolic link ~/wrk/projects/rpc-buffer-fix pointing to pw-01.

  3. If you use Antigravity (known internally at Google as Jetski), registers rpc-buffer-fix in your left sidebar automatically.

3. Run a Second Agent in Parallel#

While your first agent builds and tests inside ~/wrk/projects/rpc-buffer-fix, you can launch a second project without waiting or clobbering files:

$ ./gh wt use sensor-driver
✓ Project "sensor-driver" mounted in slot pw-02 (branch: sensor-driver)
  Directory: /home/user/wrk/projects/sensor-driver

Because pw-01 and pw-02 share a deduplicated repository cache and an 80 GB disk cache, the second slot reuses downloaded dependencies and compiled artifacts immediately.

4. Monitor All Workstreams#

Run ./gh wt list at any time to see a live dashboard combining local Git status with Gerrit review and CI checks:

$ ./gh wt list
MOUNTED PROJECTS (2/10 Slots Occupied, 8 Available)
=====================================================
PROJECT          SLOT   STATUS              GERRIT CL & DETAILS                             RECOMMENDED ACTION
rpc-buffer-fix   pw-01  🔥 NEEDS_ATTENTION  pwrev/471888 (CR:+1, 2 threads, 0 failing)      Inspect via `./gh pr view --comments`
sensor-driver    pw-02  ✨ CLEAN_SYNCED     Synced with origin/main                         Ready for hacking

5. Shelve or Advance Projects#

When you are waiting on code review and want to free a slot without losing your branch or Gerrit tracking, park the project:

$ ./gh wt park rpc-buffer-fix
💤 Parked project "rpc-buffer-fix" (slot freed; branch and Gerrit CL remain tracked)

When a CL merges on a long-lived topic project and you are ready to start the next CL in the same area, rebase the slot onto origin/main in place:

$ ./gh wt next sensor-driver
✨ Rebased project "sensor-driver" onto origin/main in-place! Ready for next CL.

CLI User Guide#

The ./gh wt command tree manages the lifecycle of slots and projects.

Initializing and Inspecting the Environment#

Run ./gh wt init to configure the slot pool, install the Gerrit commit-msg hook in the primary repository, and configure the shared Bazel cache snippet:

# Perform a read-only diagnostic check of the environment:
$ ./gh wt init --check

# Initialize or repair the environment with 10 physical slots:
$ ./gh wt init --slots 10

Mounting or Resuming a Project#

Use ./gh wt use [<project>] to allocate a warm slot for a new or parked project:

# Mount a project (creates branch if needed and updates ~/wrk/projects/<name>):
$ ./gh wt use rpc-buffer-fix

# Mount a project linked to a Buganizer issue (auto-slugs project & branch name):
$ ./gh wt use --issue 315378787

# Shorthand equivalent using Buganizer prefix:
$ ./gh wt use b/315378787

# Mount a project associated with an existing Gerrit CL:
$ ./gh wt use sensor-driver --cl 477945

# Return machine-readable JSON metadata for automated agent scripts:
$ ./gh wt use rpc-buffer-fix --json

Viewing the Project Dashboard#

Run ./gh wt list to inspect both MOUNTED and PARKED projects along with their live Git working tree status, linked Buganizer issue IDs, and Gerrit code review state:

$ ./gh wt list
MOUNTED PROJECTS (2/10 Slots Occupied, 8 Available)
=====================================================
PROJECT          SLOT   STATUS              GERRIT CL & DETAILS                                     RECOMMENDED ACTION
rpc-buffer-fix   pw-01  🔥 NEEDS_ATTENTION  b/315378787 • pwrev/471888 (CR:+1, 2 threads, 0 failing)  Inspect via `./gh pr view --comments`
sensor-driver    pw-02  ✨ CLEAN_SYNCED     Synced with origin/main                                 Ready for hacking

PARKED PROJECTS (1 Shelved in Git/Gerrit — 0 Slots Used)
=========================================================
PROJECT          SLOT   STATUS              GERRIT CL & DETAILS                                     RECOMMENDED ACTION
bazel-cleanup    -      🚀 READY_TO_LAND    pwrev/470111 (CR+2, CQ ready)                           Approved! Ready to land (`./gh pr merge --cq`)

Status Badges#

./gh wt list classifies projects using six status badges:

Badge

Condition

Recommended Action

CLEAN_SYNCED

Clean working tree with zero commits ahead of origin/main.

Ready for new work.

LOCAL_WIP

Uncommitted edits or local commits not yet uploaded to Gerrit.

Commit changes or upload via ./gh pr create.

IN_REVIEW

Open Gerrit CL awaiting reviewer feedback or CI completion.

Safe candidate to shelve via ./gh wt park.

🔥 NEEDS_ATTENTION

Open Gerrit CL with unresolved comment threads, negative CR score, or failing CI checks.

Inspect feedback via ./gh pr view --comments or ./gh pr checks.

🚀 READY_TO_LAND

Open Gerrit CL with Code-Review+2 and no blocking threads or failures.

Submit via ./gh pr merge --cq.

🎉 CL_MERGED

Associated Gerrit CL has been merged into origin/main.

Run ./gh wt next to rebase for the next CL, or close the project.

Integration with Buganizer Issues (gh issue)#

./gh wt integrates with Issue Tracking (gh issue) to link local worktree slots with Buganizer issues:

  • Issue-driven slot allocation: Running ./gh wt use --issue 315378787 (or ./gh issue develop 315378787 --worktree) fetches the issue title from Buganizer, derives a clean slug for both the project symlink and Git branch (such as b-315378787-fix-channel-framing), mounts a warm slot, and persists the issue ID in the worktree metadata.

  • Zero-commit context resolution: Inside a mounted worktree project, commands such as ./gh issue view, ./gh issue comment, and ./gh issue close automatically infer the target issue ID from the branch name or worktree metadata even before any Git commits or Bug: trailers exist on HEAD.

  • Cross-tool visibility: ./gh wt list displays linked issue IDs (b/<id>) alongside Gerrit CL details, while ./gh issue status displays [📂 MOUNTED: pw-XX] or [💤 PARKED] badges next to issues with active worktrees.

  • Lifecycle reminders: When you close a project via ./gh wt close, if an associated Buganizer issue is still open, ./gh wt prints a reminder to resolve the issue via ./gh issue close <id>.

Rebasing Persistent Projects for the Next CL#

For long-lived topic areas where you develop multiple sequential CLs, you do not need to close a project after its CL merges. Run ./gh wt next to fetch origin and rebase your mounted slot onto origin/main in place:

$ ./gh wt next rpc-buffer-fix

Parking and Closing Projects#

To explicitly free a physical slot while preserving your branch and Gerrit CL tracking entry in ./gh wt list, park the project:

$ ./gh wt park rpc-buffer-fix

When a workstream is permanently complete, close the project to remove its symbolic link and tracking entry:

$ ./gh wt close rpc-buffer-fix

Cleaning Orphaned Bazel Output Bases#

If you manually delete unmanaged Git worktrees outside the slot pool, their Bazel output bases in ~/.cache/bazel/_bazel_$USER/ may remain on disk. Run ./gh wt gc to identify and remove output bases whose workspace directories no longer exist on disk:

# Preview orphaned output bases without deleting files:
$ ./gh wt gc --dry-run

# Remove orphaned output bases:
$ ./gh wt gc

Antigravity IDE Integration#

When you run ./gh wt on a host environment with Antigravity (known internally at Google as Jetski), the tool automatically synchronizes project workspaces with your IDE sidebar. ./gh wt detects configuration directories at ~/.gemini/config/projects/, ~/.antigravity/config/projects/, or ~/.config/antigravity/projects/.

  • Automatic sidebar registration: Running ./gh wt use <project> generates a deterministic UUID v5 configuration file in your active Antigravity projects directory pointing to ~/wrk/projects/<project>. If the project is linked to a Buganizer issue, the sidebar entry is titled pw: b/<id> - <name>. The IDE server detects file updates via fsnotify and adds the project to your sidebar immediately.

  • Lifecycle archival: When a project transitions to PARKED (via manual ./gh wt park or automatic LRU eviction) or is closed via ./gh wt close, ./gh wt sets "archived": true in its project configuration file. This removes inactive projects from your active sidebar while retaining access to your past agent conversation transcripts.

  • Schema preservation and safety: When updating project JSON files, the IDE driver clones unknown JSON fields from existing entries to maintain forward compatibility. During ./gh wt init, a schema canary check verifies that the host server accepts the configuration format; if divergence is detected, IDE synchronization disables itself automatically without interrupting your CLI worktree operations.

External IDEs & CLI Agents#

You can use ./gh wt across standard Linux and macOS workstations, third-party IDEs (such as VS Code, Neovim, Cursor, or Zed), and standalone CLI agent harnesses.

  • Standard POSIX symbolic links: Because ~/wrk/projects/<project> is a standard filesystem symbolic link, you can open ~/wrk/projects/<project> directly in any editor, terminal multiplexer, or command-line agent without IDE-specific plugins.

  • Agent CLI protocol: You or your automation scripts can pass the --json flag to ./gh wt use <project> --json to obtain structured paths (slot_path, symlink_path, branch, and mode) and execute subsequent commands inside symlink_path.

  • Optional IDE synchronization: If no Antigravity project configuration directory is present on your host, the IDE driver automatically operates as a no-op. If you prefer to manage IDE workspaces manually on an Antigravity host, you can disable IDE synchronization explicitly by setting the environment variable GH_ISH_IDE_SYNC=0.

Architecture & How It Works#

Under the hood, Bazel resolves symbolic links using realpath() before computing the MD5 hash of the workspace path to select an output base directory (~/.cache/bazel/_bazel_$USER/<md5_of_realpath>/). If you create a new directory for every task, each directory receives a cold output base and triggers a full analysis phase and rebuild.

Two-Layer Directory Model#

To keep builds fast while giving each task an intuitive name, ./gh wt decouples the physical workspace path from the semantic project name:

  1. Physical Worktree Pool (Slot Layer): A fixed pool of N Git worktrees (by default 10) resides at ~/wrk/slots/pw-01 through ~/wrk/slots/pw-10. Because the physical path ~/wrk/slots/pw-XX is reused across tasks, its Bazel output base and analysis cache remain warm.

  2. Semantic Project Symlinks (Project Layer): Human-readable symbolic links reside at ~/wrk/projects/<project-name> and point to the assigned physical slot (for example, ~/wrk/projects/rpc-buffer-fix -> ~/wrk/slots/pw-03).

Residency States: Mounted vs. Parked#

A project managed by ./gh wt is always in one of two residency states:

Residency State

Physical Slot

Symlink Target

Description

MOUNTED

Assigned (pw-01 .. pw-N)

~/wrk/projects/<name> -> ~/wrk/slots/pw-XX

Active on disk with a live Git worktree and warm Bazel output base.

PARKED

None (0 slots used)

~/wrk/projects/<name>.parked marker

Shelved in Git and Gerrit. Consumes zero slot capacity while remaining tracked in ./gh wt list.

Managing More Projects Than Physical Slots#

You can track more active projects than you have physical slots configured (for example, juggling 8 concurrent projects across 5 physical slots). When you run ./gh wt use <project> and all N slots are occupied:

  • ./gh wt identifies mounted projects with clean working trees whose commits are already pushed or uploaded to Gerrit (such as projects in IN_REVIEW or CLEAN_SYNCED state).

  • The least-recently-used safe candidate is automatically parked: its slot is reassigned to your incoming project, while its branch and Gerrit CL remain tracked under the PARKED section of ./gh wt list.

  • Dirty tree protection: Projects with uncommitted local edits or unpushed commits are pinned and are never automatically evicted.

Shared Bazel Cache Configuration#

./gh wt init writes a shared Bazel configuration file to ~/.config/pw_ghish/bazelrc.worktrees and adds a conditional try-import to ~/.bazelrc:

# Managed by ./gh wt init
build --disk_cache=~/.cache/bazel-disk-cache
build --repository_cache=~/.cache/bazel-repo-cache
build --experimental_guard_against_concurrent_changes

This configuration allows all slots to share compiled object artifacts and downloaded external repositories while preventing race conditions during concurrent builds.