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 worktreedirectories 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:
Allocates warm physical slot
pw-01and checks out branchrpc-buffer-fixtrackingorigin/main.Creates the symbolic link
~/wrk/projects/rpc-buffer-fixpointing topw-01.If you use Antigravity (known internally at Google as Jetski), registers
rpc-buffer-fixin 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 working tree with zero commits ahead of |
Ready for new work. |
|
Uncommitted edits or local commits not yet uploaded to Gerrit. |
Commit changes or upload via |
|
Open Gerrit CL awaiting reviewer feedback or CI completion. |
Safe candidate to shelve via |
|
Open Gerrit CL with unresolved comment threads, negative CR score, or failing CI checks. |
Inspect feedback via |
|
Open Gerrit CL with |
Submit via |
|
Associated Gerrit CL has been merged into |
Run |
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 asb-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 closeautomatically infer the target issue ID from the branch name or worktree metadata even before any Git commits orBug:trailers exist onHEAD.Cross-tool visibility:
./gh wt listdisplays linked issue IDs (b/<id>) alongside Gerrit CL details, while./gh issue statusdisplays[📂 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 wtprints 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 titledpw: b/<id> - <name>. The IDE server detects file updates viafsnotifyand adds the project to your sidebar immediately.Lifecycle archival: When a project transitions to
PARKED(via manual./gh wt parkor automatic LRU eviction) or is closed via./gh wt close,./gh wtsets"archived": truein 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
--jsonflag to./gh wt use <project> --jsonto obtain structured paths (slot_path,symlink_path,branch, andmode) and execute subsequent commands insidesymlink_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:
Physical Worktree Pool (Slot Layer): A fixed pool of
NGit worktrees (by default 10) resides at~/wrk/slots/pw-01through~/wrk/slots/pw-10. Because the physical path~/wrk/slots/pw-XXis reused across tasks, its Bazel output base and analysis cache remain warm.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 |
|---|---|---|---|
|
Assigned ( |
|
Active on disk with a live Git worktree and warm Bazel output base. |
|
None (0 slots used) |
|
Shelved in Git and Gerrit. Consumes zero slot capacity while remaining
tracked in |
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 wtidentifies mounted projects with clean working trees whose commits are already pushed or uploaded to Gerrit (such as projects inIN_REVIEWorCLEAN_SYNCEDstate).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
PARKEDsection of./gh wt list.Dirty tree protection: Projects with uncommitted local edits or unpushed commits are pinned and are never automatically evicted.