Skip to main content

Overview

Git worktrees enable multiple agents to work on different tasks simultaneously without conflicts. Each worker gets an isolated working directory on its own branch, while sharing the same .git repository.

Why Worktrees?

True Parallel Work

10 agents can work on 10 different tasks at the same time, each in their own worktree

Shared Git History

All worktrees share the same .git directory, saving disk space and keeping history in sync

No Branch Conflicts

Each worktree has a different branch checked out, preventing accidental overwrites

Fast Cleanup

Remove a worktree without affecting others or the main workspace

Architecture

project-root/
├── .git/                     # Shared git repository
├── .stoneforge/
│   ├── .worktrees/           # Isolated worktrees
│   │   ├── e-worker-1-auth/   # Worker 1: auth feature
│   │   │   ├── src/
│   │   │   ├── package.json
│   │   │   └── ...             # Full project checkout
│   │   ├── e-worker-2-db/     # Worker 2: database migration
│   │   │   ├── src/
│   │   │   └── ...
│   │   └── p-worker-1/        # Persistent worker session
│   │       ├── src/
│   │       └── ...
│   └── db.sqlite           # Shared Stoneforge database
├── src/                     # Main worktree (default)
├── package.json
└── ...
The .stoneforge/.worktrees/ directory is automatically added to .gitignore during initialization. Do not commit worktrees to git.

Branch Naming Conventions

Stoneforge generates branch names automatically based on agent type:

Ephemeral Workers

agent/{worker-name}/{task-id}-{slug}
Examples:
  • agent/e-worker-1/task-abc123-implement-oauth
  • agent/fe-worker-2/task-def456-add-user-profile

Persistent Workers

session/{worker-name}-{timestamp}
Examples:
  • session/p-worker-1-20260302-143022
  • session/dev-alice-20260302-095511

Merge Stewards

steward/{steward-name}/{task-id}-review
Examples:
  • steward/m-steward-1/task-abc123-review
  • steward/merge-bot/task-def456-review

Worktree Lifecycle

1

Task Assignment

When a task is assigned to a worker, the dispatch daemon:
  1. Generates a branch name based on the task ID and title
  2. Creates a worktree path: .stoneforge/.worktrees/{agent-name}-{slug}
  3. Stores metadata in the task’s orchestrator metadata
2

Worktree Creation

The worker spawner creates the worktree:
const result = await worktreeManager.createWorktree({
  agentName: 'e-worker-1',
  taskId: 'task-abc123',
  taskTitle: 'Implement OAuth login',
  baseBranch: 'main',
});

// result.branch: 'agent/e-worker-1/task-abc123-implement-oauth'
// result.path: '.stoneforge/.worktrees/e-worker-1-implement-oauth'
This runs:
git fetch origin main
git worktree add -b agent/e-worker-1/task-abc123-implement-oauth \
  .stoneforge/.worktrees/e-worker-1-implement-oauth \
  origin/main
3

Worker Execution

The worker agent:
  1. Spawns in the worktree directory
  2. Works on the task (writes code, commits, pushes)
  3. Completes or hands off the task
4

Merge Review

The merge steward:
  1. Checks out the worker’s branch in its own worktree
  2. Reviews changes, resolves conflicts, runs tests
  3. Merges to main (squash or merge strategy)
5

Cleanup

After successful merge:
git worktree remove .stoneforge/.worktrees/e-worker-1-implement-oauth
git branch -d agent/e-worker-1/task-abc123-implement-oauth
git push origin --delete agent/e-worker-1/task-abc123-implement-oauth

Working with Worktrees

Creating Worktrees Manually

For testing or debugging, create worktrees directly:
import { createWorktreeManager } from '@stoneforge/smithy/git';

const manager = createWorktreeManager({
  workspaceRoot: process.cwd(),
  worktreeDir: '.stoneforge/.worktrees',
});

await manager.initWorkspace();

const result = await manager.createWorktree({
  agentName: 'test-worker',
  taskId: 'task-test-123',
  taskTitle: 'Test worktree creation',
});

console.log('Worktree created at:', result.path);
console.log('Branch:', result.branch);

Listing Active Worktrees

// List all worktrees (excluding main)
const worktrees = await manager.listWorktrees();

for (const wt of worktrees) {
  console.log(`${wt.branch} -> ${wt.relativePath}`);
  console.log(`  State: ${wt.state}`);
  console.log(`  Agent: ${wt.agentName}`);
}

// Get worktrees for specific agent
const agentWorktrees = await manager.getWorktreesForAgent('e-worker-1');

Removing Worktrees

// Remove worktree and delete branch
await manager.removeWorktree('.stoneforge/.worktrees/e-worker-1-auth', {
  deleteBranch: true,
  deleteRemoteBranch: true,
  force: false,
});

// Force remove (even with uncommitted changes)
await manager.removeWorktree('.stoneforge/.worktrees/stuck-worker', {
  force: true,
  deleteBranch: true,
  forceBranchDelete: true,
});

Worktree States

Worktrees transition through lifecycle states:
StateDescriptionValid Transitions
creatingBeing initializedactive, cleaning
activeIn use by agentsuspended, merging, cleaning
suspendedPaused, can resumeactive, cleaning
mergingBranch being mergedarchived, cleaning, active
cleaningBeing removedarchived
archivedRemoved(terminal state)

Suspending and Resuming

Temporarily pause a worktree without removing it:
// Suspend (agent stops, worktree preserved)
await manager.suspendWorktree('.stoneforge/.worktrees/e-worker-1-auth');

// Resume later
await manager.resumeWorktree('.stoneforge/.worktrees/e-worker-1-auth');
Suspended worktrees are useful for persistent workers that need to pause between tasks.

Dependency Installation

Worktrees can auto-install dependencies on creation:
const result = await manager.createWorktree({
  agentName: 'e-worker-1',
  taskId: 'task-abc123',
  taskTitle: 'Add new feature',
  installDependencies: true,  // Auto-run package manager
});
The manager detects your package manager and runs the appropriate install:
  • pnpm: pnpm install --frozen-lockfile (or pnpm install if lockfile out of sync)
  • bun: bun install --frozen-lockfile
  • yarn: yarn install --frozen-lockfile
  • npm: npm ci (or npm install if no package-lock.json)
Dependency installation adds 30-60s to worktree creation time. Only enable for projects that need it.

Handling Conflicts

Merge Conflicts

When merging a worker’s branch, conflicts may occur:
# In the merge steward's worktree
git status
# Shows conflicted files

# Resolve conflicts manually
vim src/file-with-conflict.ts

# Commit the resolution
git add .
git commit -m "Resolve merge conflicts with main"

# Continue with merge
sf task merge <task-id>

Worktree Directory Conflicts

If a worktree directory already exists:
try {
  await manager.createWorktree({
    agentName: 'e-worker-1',
    taskId: 'task-abc123',
    taskTitle: 'Feature',
  });
} catch (error) {
  if (error.code === 'WORKTREE_EXISTS') {
    // Remove stale worktree and retry
    await manager.removeWorktree('.stoneforge/.worktrees/e-worker-1-feature', {
      force: true,
    });
    
    // Retry creation
    await manager.createWorktree({
      agentName: 'e-worker-1',
      taskId: 'task-abc123',
      taskTitle: 'Feature',
    });
  }
}

Performance Considerations

Disk Space

Each worktree creates a full working directory:
# Main workspace
du -sh .
# 2.5G

# Each worktree
du -sh .stoneforge/.worktrees/e-worker-1-auth
# 2.3G (node_modules is the bulk)
10 worktrees = ~25GB disk usage
Use installDependencies: false for worktrees that don’t need to run tests locally. They can still build and commit code.

Worktree Creation Speed

OperationTime
git worktree add~100ms
+ pnpm install --frozen-lockfile+30-60s
+ npm ci+60-120s

Cleanup Best Practices

// Clean up old worktrees periodically
const worktrees = await manager.listWorktrees();

for (const wt of worktrees) {
  // Remove archived or failed worktrees
  if (wt.state === 'archived' || wt.state === 'cleaning') {
    await manager.removeWorktree(wt.path, { force: true });
  }
}

// Prune stale worktree entries
// (worktrees deleted manually outside of Stoneforge)
execSync('git worktree prune', { cwd: workspaceRoot });

Read-Only Worktrees

For triage sessions that shouldn’t create branches:
const result = await manager.createReadOnlyWorktree({
  agentName: 'triage-worker',
  purpose: 'inbox-triage',
});

// Creates detached HEAD worktree (no branch)
console.log(result.branch); // '(detached-inbox-triage)'
Read-only worktrees:
  • Don’t create new branches
  • Check out detached HEAD on the base branch
  • Useful for analysis tasks that don’t modify code

Common Patterns

Worktree Per Task

Ephemeral workers get a fresh worktree for each task:
// Task 1: OAuth implementation
const task1Worktree = await manager.createWorktree({
  agentName: 'e-worker-1',
  taskId: 'task-001',
  taskTitle: 'Implement OAuth',
});

// After completion and merge, cleanup
await manager.removeWorktree(task1Worktree.path, {
  deleteBranch: true,
});

// Task 2: New worktree for next task
const task2Worktree = await manager.createWorktree({
  agentName: 'e-worker-1',
  taskId: 'task-002', 
  taskTitle: 'Add user profile',
});

Long-Lived Worktree

Persistent workers reuse a session worktree:
// Create session worktree once
const sessionWorktree = await manager.createWorktree({
  agentName: 'p-worker-1',
  taskId: 'session-main',
  taskTitle: '',  // No slug needed
  customBranch: `session/p-worker-1-${timestamp}`,
});

// Worker uses this worktree for multiple tasks
// Merges completed work with `sf merge`
// Worktree stays active for next task

Steward Review Worktree

Merge stewards get temporary worktrees for review:
// Create review worktree
const reviewWorktree = await manager.createWorktree({
  agentName: 'm-steward-1',
  taskId: 'task-abc123',
  taskTitle: 'review',
  customBranch: 'agent/e-worker-1/task-abc123-implement-oauth', // Existing branch
});

// After merge, cleanup
await manager.removeWorktree(reviewWorktree.path, {
  deleteBranch: true,
  deleteRemoteBranch: true,
});

Troubleshooting

Error: WORKTREE_EXISTSCause: Directory left over from previous task or crashSolution:
// Force remove and recreate
await manager.removeWorktree(path, { force: true });
await manager.createWorktree({ ... });
Error: fatal: 'branch-name' is already checked out at '...'Cause: Same branch checked out in multiple worktreesSolution: Each worktree must have a unique branch. Remove the conflicting worktree:
git worktree list
git worktree remove <path-to-conflicting-worktree>
Symptom: git worktree list shows worktrees that don’t exist on diskSolution: Prune stale entries:
git worktree prune
Or in code:
await manager.initWorkspace(); // Automatically prunes on init
Symptom: Worktree creation fails with “No space left on device”Solution: Clean up old worktrees or disable dependency installation:
// List and remove old worktrees
const worktrees = await manager.listWorktrees();
for (const wt of worktrees) {
  if (wt.state === 'archived') {
    await manager.removeWorktree(wt.path, { force: true });
  }
}

// Create worktree without dependencies
await manager.createWorktree({
  agentName: 'e-worker-1',
  taskId: 'task-abc123',
  taskTitle: 'Feature',
  installDependencies: false,  // Save ~2GB per worktree
});

CLI Reference

While the orchestrator manages worktrees automatically, you can inspect them with git:
# List all worktrees
git worktree list

# Add worktree manually (for testing)
git worktree add -b test-branch .stoneforge/.worktrees/test origin/main

# Remove worktree
git worktree remove .stoneforge/.worktrees/test

# Prune stale worktree metadata
git worktree prune

# Check worktree status
cd .stoneforge/.worktrees/e-worker-1-auth
git status

Merge Review

How merge stewards review and merge work

Scaling Agents

Run multiple agents in parallel

Custom Agents

Create specialized agent roles

Build docs developers (and LLMs) love