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-slackStep 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
opencodeIntegration 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-slackAdvantages Over Nested Pattern
| Aspect | Nested (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-slackGateway 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:
- Create builder directory (container)
- Add workspace-root as sibling worktree
- Add project repos as sibling worktrees
- Start OpenCode at builder root
- 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.