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
Task Assignment
When a task is assigned to a worker, the dispatch daemon:
Generates a branch name based on the task ID and title
Creates a worktree path: .stoneforge/.worktrees/{agent-name}-{slug}
Stores metadata in the task’s orchestrator metadata
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
Worker Execution
The worker agent:
Spawns in the worktree directory
Works on the task (writes code, commits, pushes)
Completes or hands off the task
Merge Review
The merge steward:
Checks out the worker’s branch in its own worktree
Reviews changes, resolves conflicts, runs tests
Merges to main (squash or merge strategy)
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:
State Description Valid Transitions creatingBeing initialized → active, cleaning activeIn use by agent → suspended, merging, cleaning suspendedPaused, can resume → active, cleaning mergingBranch being merged → archived, cleaning, active cleaningBeing removed → archived 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-i d >
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' ,
});
}
}
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
Operation Time 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-worktre e >
Symptom : git worktree list shows worktrees that don’t exist on diskSolution : Prune stale entries: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