How to Manage Local Repositories, Worktrees, and Git Operations with Routa

Routa isolates agent operations by treating Git worktrees as first-class resources, creating disposable environments tracked in a database and serializing all Git commands through repository-specific locks to prevent race conditions.

Managing concurrent Git operations across multiple AI agents requires strict isolation. Routa solves this through a dedicated worktree architecture centered in src/core/git/git-worktree-service.ts. This service creates isolated working directories for each task, persists their metadata to a database, and guarantees thread-safe access to the underlying Git repository.

Core Architecture: The GitWorktreeService

The GitWorktreeService class is the single source of truth for all worktree operations in Routa. It encapsulates the complexity of Git worktree management behind a unified API that the Kanban orchestrator, UI panels, and background tasks consume.

Key responsibilities include:

  • Persistence management – Maintains a WorktreeStore database record for every worktree, ensuring the UI can query current state even after process restarts.
  • Path generation – Calculates filesystem-safe paths under ~/.routa/worktrees/<workspace>/<codebase>/<branch-dir>, with optional overrides via options.worktreeRoot.
  • Git abstraction – Routes all Git commands through the platform bridge's execGit function, which handles proper escaping and configurable timeouts for both Node server and Tauri desktop environments.

As implemented in phodal/routa, the service uses an internal repoLocks Map to serialize operations per repository, preventing corruption of the .git/worktrees directory when multiple agents act simultaneously.

The Worktree Lifecycle

Routa manages worktrees through a predictable seven-step workflow that spans from task creation to cleanup.

Request and Creation

When a task enters a development column, the orchestrator calls GitWorktreeService.createWorktree(codebaseId, opts) at line 98 of the service file. This method generates a unique branch name, creates the worktree directory, and persists the record.

Branch names follow the pattern wt/<label-or-uuid> and are sanitized using the internal branchToSafeDirName utility (line 47) to ensure compatibility across Windows, macOS, and Linux filesystems.

Filesystem Layout

By default, worktrees live at:

~/.routa/worktrees/<workspace>/<codebase>/<branch-dir>

The getWorktreeBaseDir helper (line 53) constructs this path, respecting the options.worktreeRoot override when provided. This isolation prevents agents from interfering with the main working directory or other active worktrees.

Git Operations and Persistence

All Git invocations flow through execGit (line 30), which executes commands via the platform bridge. After the Git worktree is created on disk, the service inserts a Worktree row via worktreeStore.add, linking the filesystem path to the task model defined in src/core/models/task.ts.

Validation and Health Checks

Before allowing a user to switch to a task, the UI calls validateWorktree (line 64). This method verifies directory existence and checks for the presence of a .git file (not directory), confirming the worktree remains healthy after crashes or long pauses.

Cleanup and Removal

When tasks complete or stale worktrees are detected, GitWorktreeService.removeWorktree (line 8) removes the directory and optionally deletes the associated branch via the deleteBranch option. This automatic cleanup prevents disk space exhaustion in long-running Routa instances.

Concurrency Safety with Repository Locks

Routa prevents race conditions through a repoLocks Map that stores Promise<void> objects keyed by repository path. The withRepoLock wrapper sets the lock before any await statement, ensuring that only one operation touches a given repository's .git/worktrees metadata at a time.

The lock releases automatically in a finally block, guaranteeing that concurrent requests to create, remove, or prune worktrees for the same codebase execute sequentially rather than interleaving.

Database Integration

The service relies on WorktreeStore implementations (typically pg-worktree-store.ts for PostgreSQL or in-memory variants for testing) to maintain state. Each worktree record includes:

  • id: UUID reference stored in task.worktreeId
  • codebaseId: Link to the parent repository
  • worktreePath: Absolute filesystem location
  • branch: Associated Git branch name
  • status: Current lifecycle state

This persistence layer allows the Kanban orchestrator in src/core/kanban/workflow-orchestrator-singleton.ts to provision worktrees asynchronously while the UI polls for status updates.

Practical Implementation Examples

Creating a Worktree for a Task

import { GitWorktreeService } from '@/core/git/git-worktree-service';
import { InMemoryWorktreeStore } from '@/core/db/pg-worktree-store';
import { InMemoryCodebaseStore } from '@/core/db/pg-codebase-store';

const worktreeService = new GitWorktreeService(
  new InMemoryWorktreeStore(),
  new InMemoryCodebaseStore()
);

async function provisionWorktree(codebaseId: string) {
  const wt = await worktreeService.createWorktree(codebaseId, {
    label: 'feature-xyz',
    // Optional: override default ~/.routa/worktrees location
    // worktreeRoot: '/tmp/custom-worktrees',
  });
  console.log('Worktree ready:', wt.worktreePath);
  return wt.id; // Store this on the task model
}

Listing Worktrees for the UI Panel

async function listForCodebase(codebaseId: string) {
  const worktrees = await worktreeService.listWorktrees(codebaseId);
  worktrees.forEach(wt => {
    console.log(`${wt.branch} → ${wt.worktreePath} [${wt.status}]`);
  });
}

Validating Before User Access

async function ensureHealthy(worktreeId: string) {
  const { healthy, error } = await worktreeService.validateWorktree(worktreeId);
  if (!healthy) {
    throw new Error(`Worktree is broken: ${error}`);
  }
}

Cleanup After Task Completion

async function cleanupWorktree(worktreeId: string) {
  await worktreeService.removeWorktree(worktreeId, { deleteBranch: true });
  console.log('Worktree removed and branch deleted');
}

Summary

  • Centralized service – All worktree logic lives in src/core/git/git-worktree-service.ts, providing a consistent API for creation, validation, and removal.
  • Database-backed state – Worktrees persist as Worktree records via pg-worktree-store, linked to tasks through task.worktreeId.
  • Race-condition prevention – The repoLocks Map serializes Git operations per repository, protecting .git/worktrees integrity.
  • Default isolation – Worktrees reside under ~/.routa/worktrees/ with OS-safe branch naming (wt/<label-or-uuid>).
  • Lifecycle integration – The Kanban orchestrator automatically provisions worktrees when tasks enter development columns and cleans them up upon completion.

Frequently Asked Questions

How does Routa prevent Git corruption when multiple agents run simultaneously?

Routa uses a repoLocks Map to serialize operations per repository. Before executing any Git command, the service acquires a lock specific to that repository's path, ensuring that only one operation modifies the .git/worktrees directory at a time. The lock releases automatically after the operation completes, even if an error occurs.

Can I customize where Routa stores worktrees on disk?

Yes. While the default location is ~/.routa/worktrees/<workspace>/<codebase>/, you can override this by passing worktreeRoot in the options object when calling GitWorktreeService.createWorktree. This allows you to place worktrees on faster disks or dedicated volumes.

What happens if a worktree becomes corrupted or is deleted externally?

The validateWorktree method checks both directory existence and the presence of a .git file (worktrees use a .git file pointing to the main repository, not a directory). If validation fails, the UI can prompt for recreation or cleanup, ensuring agents never attempt to operate in broken environments.

How does the Kanban board connect tasks to specific worktrees?

The task model in src/core/models/task.ts includes an optional worktreeId?: string field. When the workflow orchestrator moves a task to a development column, it calls createWorktree and stores the returned UUID in this field. This creates a durable link between the task card and its isolated working directory.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →