Workspace Pattern V2 - Sibling Worktrees

Date: 2026-01-30
Version: 2.0 (Revised based on gitignore constraint)
Context: Multi-repo workspace visibility for OpenCode


The Problem (Revised Understanding)

Initial assumption: Nested repos in repos/ could be made visible via worktrees
Reality: .gitignore ignores paths regardless of whether they’re worktrees
Root cause: OpenCode only sees files tracked by git, and gitignored paths are invisible

The constraint:

  • repos/ must remain gitignored in workspace-root (independent projects)
  • Different projects need different combinations of repos
  • OpenCode needs to see changes in both workspace-root AND selected repos

The Solution: Sibling Worktrees

Create a builder workspace where workspace-root and project repos are siblings, not nested.

Pattern Overview

workspace-root/                      # Main repo (unchanged)
├── .gitignore (repos still ignored) # No changes needed!
├── repos/                           # Independent repos
│   ├── ai-dev/                      # Standalone git repo
│   ├── domain-apis/                 # Standalone git repo  
│   └── k8s-lab/                     # Standalone git repo
│
└── .builders/
    └── ai-dev-gateway-slack/        # Builder workspace (directory only)
        ├── workspace-root/          # ← Worktree of workspace-root
        │   ├── .git → worktree ref
        │   ├── codev/
        │   ├── .ai/
        │   └── Taskfile.yaml
        │
        ├── ai-dev/                  # ← Worktree of repos/ai-dev (SIBLING!)
        │   ├── .git → worktree ref
        │   ├── services/
        │   └── README.md
        │
        └── domain-apis/             # ← Worktree of repos/domain-apis (SIBLING!)
            ├── .git → worktree ref
            └── src/
        
        [OpenCode runs HERE] ← Sees all siblings!

How It Works

Step 1: Create Builder Directory

# Builder directory is just a container (NOT a worktree itself)
mkdir -p .builders/ai-dev-gateway-slack

Step 2: Add workspace-root as Sibling Worktree

# Add workspace-root as a subdirectory worktree
git worktree add .builders/ai-dev-gateway-slack/workspace-root \
  -b builder/ai-dev-gateway-slack
 
# Result:
# .builders/ai-dev-gateway-slack/
# └── workspace-root/  (worktree with its own .git)

Step 3: Add Project Repos as Sibling Worktrees

# Add ai-dev repo as sibling to workspace-root
cd repos/ai-dev
git worktree add ../../.builders/ai-dev-gateway-slack/ai-dev main
cd ../..
 
# Add domain-apis repo as another sibling
cd repos/domain-apis
git worktree add ../../.builders/ai-dev-gateway-slack/domain-apis main
cd ../..
 
# Result:
# .builders/ai-dev-gateway-slack/
# ├── workspace-root/  (worktree)
# ├── ai-dev/          (worktree)
# └── domain-apis/     (worktree)

Step 4: Start OpenCode at Builder Root

cd .builders/ai-dev-gateway-slack
opencode serve --session ai-dev-gateway-slack --project .
 
# OpenCode workspace root: .builders/ai-dev-gateway-slack/
# OpenCode sees:
# ✅ workspace-root/ (all codev files)
# ✅ ai-dev/ (target repo)
# ✅ domain-apis/ (additional repo)

Why This Works

No Gitignore Conflicts

Each worktree has its own .git and .gitignore:

.builders/ai-dev-gateway-slack/
├── workspace-root/.gitignore  → ignores "repos" (doesn't affect siblings!)
├── ai-dev/.gitignore          → ai-dev's own ignore rules
└── domain-apis/.gitignore     → domain-apis's own ignore rules

Key: workspace-root’s .gitignore saying repos doesn’t affect sibling directories!

Clean Separation

  • Each repo is a sibling, not nested
  • No parent-child gitignore conflicts
  • OpenCode sees them as separate folders in the workspace
  • Changes in any sibling are visible

Flexible Combinations

Different projects can combine different repos:

.builders/project-a/
├── workspace-root/
└── ai-dev/

.builders/project-b/
├── workspace-root/
├── domain-apis/
└── k8s-lab/

Updated builder:init Task

File: workspace-root/Taskfile.yaml

builder:init:
  desc: Initialize builder workspace with sibling repos
  vars:
    BUILDER_NAME: '{{.BUILDER_NAME | default "default"}}'
    REPOS: '{{.REPOS}}'  # Comma-separated list
  cmds:
    - |
      echo "🚀 Initializing builder: {{.BUILDER_NAME}}"
      
      # Step 1: Create builder directory
      mkdir -p .builders/{{.BUILDER_NAME}}
      
      # Step 2: Add workspace-root as sibling worktree
      echo "📦 Adding workspace-root..."
      git worktree add .builders/{{.BUILDER_NAME}}/workspace-root \
        -b builder/{{.BUILDER_NAME}}
      
      # Step 3: Add each repo as sibling worktree
      if [ -n "{{.REPOS}}" ]; then
        IFS=',' read -ra REPO_LIST <<< "{{.REPOS}}"
        for repo in "${REPO_LIST[@]}"; do
          echo "📦 Adding repo: $repo"
          
          # Check if repo exists
          if [ ! -d "repos/$repo" ]; then
            echo "⚠️  Warning: repos/$repo not found, skipping"
            continue
          fi
          
          # Add repo as sibling worktree
          (cd repos/$repo && \
           git worktree add ../../.builders/{{.BUILDER_NAME}}/$repo main)
        done
      fi
      
      echo ""
      echo "✅ Builder workspace ready!"
      echo "📁 Location: .builders/{{.BUILDER_NAME}}"
      echo ""
      echo "Structure:"
      tree -L 1 .builders/{{.BUILDER_NAME}} || ls -1 .builders/{{.BUILDER_NAME}}
      echo ""
      echo "To start working:"
      echo "  cd .builders/{{.BUILDER_NAME}}"
      echo "  opencode"

Usage:

# Initialize builder with specific repos
task builder:init BUILDER_NAME=ai-dev-gateway-slack REPOS=ai-dev,domain-apis
 
# Start OpenCode
cd .builders/ai-dev-gateway-slack
opencode

Integration with AI Dev Gateway

When a user runs /opencode start in Slack:

class WorkspaceManager:
    async def create_builder_workspace(
        self,
        category: str,
        project: str,
        repos: List[str]
    ) -> str:
        """
        Create builder workspace with sibling worktrees.
        
        Returns: Path to builder workspace
        """
        builder_name = f"{category}-{project}"
        builder_path = f".builders/{builder_name}"
        
        # Step 1: Create builder directory
        os.makedirs(builder_path, exist_ok=True)
        
        # Step 2: Add workspace-root as sibling
        await self._run_command([
            "git", "worktree", "add",
            f"{builder_path}/workspace-root",
            "-b", f"builder/{builder_name}"
        ])
        
        # Step 3: Add repos as siblings
        for repo in repos:
            await self._add_repo_sibling(builder_path, repo)
        
        return builder_path
    
    async def _add_repo_sibling(self, builder_path: str, repo: str):
        """Add a repo as sibling worktree."""
        repo_path = f"repos/{repo}"
        
        if not os.path.exists(repo_path):
            logger.warning(f"Repo {repo} not found, skipping")
            return
        
        # Create worktree from repo
        await self._run_command([
            "git", "-C", repo_path,
            "worktree", "add",
            f"../../{builder_path}/{repo}",
            "main"
        ])

File Access Patterns

Accessing workspace-root files

# In OpenCode session at .builders/ai-dev-gateway-slack/
 
# Reference workspace-root files
"workspace-root/.ai/projects/ai-dev/opencode-slack-integration/api-contract.md"
"workspace-root/codev/specs/0001-spec.md"
"workspace-root/Taskfile.yaml"

Accessing repo files

# Reference ai-dev repo files
"ai-dev/services/gateway/src/main.py"
"ai-dev/README.md"
 
# Reference domain-apis repo files
"domain-apis/src/api.py"

Running tasks

# From builder root: .builders/ai-dev-gateway-slack/
 
# Run workspace-root tasks
task -d workspace-root some:task
 
# Or CD into workspace-root
cd workspace-root
task some:task
 
# Work on ai-dev repo
cd ai-dev
# Make changes, commit, etc.

Example: Complete Session Lifecycle

# User in Slack: /opencode start
# Form: category=ai-dev, project=gateway-slack, repos=ai-dev,domain-apis
 
# Gateway Step 1: Create builder directory
$ mkdir -p .builders/ai-dev-gateway-slack
 
# Gateway Step 2: Add workspace-root worktree
$ git worktree add .builders/ai-dev-gateway-slack/workspace-root \
    -b builder/ai-dev-gateway-slack
Preparing worktree (new branch 'builder/ai-dev-gateway-slack')
HEAD is now at 0756050 fix: Correct BMAD slash commands
 
# Gateway Step 3: Add ai-dev repo worktree
$ cd repos/ai-dev
$ git worktree add ../../.builders/ai-dev-gateway-slack/ai-dev main
Preparing worktree (checking out 'main')
HEAD is now at abc123 feat: Add gateway service
 
# Gateway Step 4: Add domain-apis repo worktree
$ cd ../domain-apis
$ git worktree add ../../.builders/ai-dev-gateway-slack/domain-apis main
Preparing worktree (checking out 'main')
HEAD is now at def456 feat: Add domain API
 
# Gateway Step 5: Start OpenCode at builder root
$ cd ../../.builders/ai-dev-gateway-slack
$ opencode serve --session ai-dev-gateway-slack --project .
OpenCode server started on port 8080
 
# Developer works...
# All changes visible in OpenCode:
# - workspace-root/...
# - ai-dev/...
# - domain-apis/...
 
# When complete, cleanup:
$ git worktree remove .builders/ai-dev-gateway-slack/workspace-root
$ cd repos/ai-dev && git worktree remove ../../.builders/ai-dev-gateway-slack/ai-dev
$ cd ../domain-apis && git worktree remove ../../.builders/ai-dev-gateway-slack/domain-apis
$ rm -rf .builders/ai-dev-gateway-slack

Advantages Over Nested Pattern

AspectNested (repos/)Sibling Worktrees
Gitignore conflicts❌ Yes✅ No
OpenCode visibility❌ Hidden✅ Visible
Clean separation❌ Parent/child✅ Siblings
Flexible combinations✅ Yes✅ Yes
Workspace-root mods❌ Required✅ None needed

Cleanup

# Manual cleanup
cd .builders/ai-dev-gateway-slack
 
# Remove each worktree
git -C workspace-root worktree remove .
cd ../../repos/ai-dev && git worktree remove ../../.builders/ai-dev-gateway-slack/ai-dev
cd ../domain-apis && git worktree remove ../../.builders/ai-dev-gateway-slack/domain-apis
 
# Remove builder directory
rm -rf .builders/ai-dev-gateway-slack

Gateway automation:

async def cleanup_builder(builder_name: str):
    builder_path = f".builders/{builder_name}"
    
    # Get list of subdirectories (each is a worktree)
    for item in os.listdir(builder_path):
        item_path = os.path.join(builder_path, item)
        if os.path.isdir(item_path):
            # Remove worktree
            await self._run_command([
                "git", "-C", item_path,
                "worktree", "remove", "."
            ])
    
    # Remove builder directory
    shutil.rmtree(builder_path)

Summary

The Pattern:

  1. Create builder directory (container)
  2. Add workspace-root as sibling worktree
  3. Add project repos as sibling worktrees
  4. Start OpenCode at builder root
  5. OpenCode sees all siblings!

Why it works:

  • ✅ No gitignore conflicts (siblings, not nested)
  • ✅ Each worktree has its own .git and .gitignore
  • ✅ OpenCode scans all sibling directories
  • ✅ Clean separation, flexible combinations
  • ✅ No changes to workspace-root .gitignore needed

This is the correct pattern for multi-repo OpenCode workspaces.