Skip to main content

Monorepo Configuration

Workloom handles monorepos natively through multi-target workspaces. Each target (root, app, service, etc.) gets its own set of providers, packages, and rendered files. Targets resolve independently with no cross-target interference.

Multi-Target Workspaces

Instead of creating separate workspace manifests for each app in your monorepo, define a single workspace with multiple targets:

{
"workspaceId": "wksp_monorepo_01",
"orgId": "org_acme",
"name": "Acme Monorepo",
"binding": {
"type": "git-repository",
"gitRemote": "git@github.com:acme/monorepo.git"
},
"targets": [
{
"id": "root",
"path": ".",
"providers": ["codex", "copilot"],
"packages": [
{ "packageId": "@acme/baseline@1.0.0" }
]
},
{
"id": "frontend",
"path": "apps/web",
"providers": ["claude", "codex"],
"packages": [
{ "packageId": "@acme/baseline@1.0.0" },
{ "packageId": "@acme/react-patterns@2.1.0" }
]
},
{
"id": "backend",
"path": "services/api",
"providers": ["claude"],
"packages": [
{ "packageId": "@acme/baseline@1.0.0" },
{ "packageId": "@acme/backend-guidelines@3.0.0" }
]
}
],
"revision": 1,
"projectionPolicy": {
"claude": "generated_local",
"codex": "generated_local",
"copilot": "committed_projection"
}
}

Each Target Gets Its Own Providers and Packages

Each target is independent:

TargetPathProvidersPackages
root.Codex, CopilotBaseline
frontendapps/webClaude, CodexBaseline, React Patterns
backendservices/apiClaudeBaseline, Backend Guidelines

When you run wl pull, Workloom renders:

.                          # Root target
├── AGENTS.md # From @acme/baseline (Codex)
├── .github/
│ └── copilot-instructions.md # (Copilot)

apps/web/ # Frontend target
├── .claude/
│ ├── settings.json # From @acme/react-patterns
│ └── CLAUDE.md # (Claude)
├── AGENTS.md # From @acme/baseline + @acme/react-patterns (Codex merge)

services/api/ # Backend target
└── .claude/
├── settings.json # From @acme/backend-guidelines
└── CLAUDE.md # (Claude)

How Targets Resolve Independently

Each target:

  1. Resolves its own packages independently. No cross-target version conflicts.
  2. Renders to its own path. Files in apps/web/ never overwrite services/api/ files.
  3. Uses its own providers. Root can use Copilot while frontend uses Claude—no interference.
  4. Has its own drift state. wl status reports per-target drift separately.

Independent Package Resolution

Targets can pin different versions of the same package:

{
"targets": [
{
"id": "frontend",
"path": "apps/web",
"packages": [
{ "packageId": "@acme/baseline@1.0.0" } // Version 1.0.0
]
},
{
"id": "backend",
"path": "services/api",
"packages": [
{ "packageId": "@acme/baseline@2.0.0" } // Version 2.0.0
]
}
]
}

Each target renders its own version of the baseline package without conflict. Lock files track the resolved state per target.

No Cross-Target Overwrites

Files rendered for one target never overwrite another:

✓ apps/web/.claude/settings.json  (frontend target)
✓ services/api/.claude/settings.json (backend target)
✗ apps/web/.claude/settings.json (frontend) won't overwrite services/api (backend)

How Drift Detection Works Per-Target

When you run wl status, Workloom reports drift per target:

wl status
# Workloom Status Report
# Workspace: Acme Monorepo

# Target: root
# Providers: codex, copilot
# Status: owned_clean
# Drift: None

# Target: frontend
# Providers: claude, codex
# Status: owned_locally_modified
# Files: AGENTS.md (drift), .claude/CLAUDE.md (clean)

# Target: backend
# Providers: claude
# Status: owned_clean
# Drift: None

See details for a specific target:

wl status --target frontend
# Target: frontend (apps/web/)
# Packages: @acme/baseline@1.0.0, @acme/react-patterns@2.1.0
# Drift:
# AGENTS.md (state: owned_locally_modified)

Check differences:

wl diff --target frontend
# apps/web/AGENTS.md differs from desired state
# (unified diff)

Restore a specific target:

wl restore --target frontend
# Restored apps/web/ to desired state

Path Resolution and Precedence

File paths in rendered assets are relative to the target's path:

Claude Asset in Frontend Target

If you have an instruction asset in @acme/react-patterns:

{
"name": "react-tips",
"type": "instruction",
"provider": "claude",
"path": ".claude/CLAUDE.md",
"content": "# React Tips for Claude\n\n..."
}

And the target is:

{
"id": "frontend",
"path": "apps/web"
}

The file renders to:

apps/web/.claude/CLAUDE.md

Not ./.claude/CLAUDE.md at the root.

Nested Asset Paths

If an asset specifies a relative path:

{
"path": ".claude/skills/react/SKILL.md"
}

And the target is services/api/, the asset renders to:

services/api/.claude/skills/react/SKILL.md

Example: Three-Target Monorepo Workspace

A realistic monorepo with shared + target-specific packages:

{
"workspaceId": "wksp_ecommerce_01",
"orgId": "org_shop",
"name": "E-Commerce Monorepo",
"binding": {
"type": "git-repository",
"gitRemote": "git@github.com:shop/platform.git"
},
"targets": [
{
"id": "root",
"path": ".",
"providers": ["codex", "copilot"],
"packages": [
{
"packageId": "@shop/engineering-baseline@3.2.0",
"channel": "stable"
}
]
},
{
"id": "storefront",
"path": "apps/storefront",
"providers": ["claude", "codex"],
"packages": [
{
"packageId": "@shop/engineering-baseline@3.2.0",
"channel": "stable"
},
{
"packageId": "@shop/frontend-patterns@2.0.0",
"channel": "stable"
}
]
},
{
"id": "api",
"path": "services/api",
"providers": ["claude"],
"packages": [
{
"packageId": "@shop/engineering-baseline@3.2.0",
"channel": "stable"
},
{
"packageId": "@shop/backend-guidelines@1.5.0",
"channel": "stable"
}
]
},
{
"id": "admin",
"path": "apps/admin",
"providers": ["claude", "codex"],
"packages": [
{
"packageId": "@shop/engineering-baseline@3.2.0",
"channel": "stable"
},
{
"packageId": "@shop/frontend-patterns@2.0.0",
"channel": "stable"
},
{
"packageId": "@shop/security-policies@1.1.0",
"channel": "stable"
}
]
}
],
"revision": 5,
"projectionPolicy": {
"claude": "generated_local",
"codex": "generated_local",
"copilot": "committed_projection"
},
"ciPolicy": {
"requireCleanSync": true,
"failOnDrift": true
}
}

After wl pull:

.                                  # root target
├── AGENTS.md # Codex: baseline
├── .github/copilot-instructions.md # Copilot: baseline

apps/storefront/ # storefront target
├── .claude/
│ ├── settings.json # Claude: baseline + frontend-patterns
│ └── CLAUDE.md
├── AGENTS.md # Codex: baseline + frontend-patterns merge

services/api/ # api target
└── .claude/
├── settings.json # Claude: baseline + backend-guidelines
└── CLAUDE.md

apps/admin/ # admin target
├── .claude/
│ ├── settings.json # Claude: baseline + frontend-patterns + security
│ └── CLAUDE.md
└── AGENTS.md # Codex: baseline + frontend-patterns + security merge

Common Patterns

Shared Base + Target-Specific Packages

All targets get a baseline package; additional packages depend on role:

"packages": [
{ "packageId": "@org/baseline@1.0.0" }, // Always included
{ "packageId": "@org/frontend-patterns@2.0.0" }, // Only frontend apps
{ "packageId": "@org/backend-guidelines@1.5.0" } // Only backend services
]

Different Provider Strategies Per Target

Root uses Copilot (committed, centralized), frontend uses Claude (local, personalized):

{
"targets": [
{
"id": "root",
"providers": ["copilot"],
"packages": [{ "packageId": "@org/shared@1.0.0" }]
},
{
"id": "frontend",
"providers": ["claude"],
"packages": [{ "packageId": "@org/shared@1.0.0" }]
}
],
"projectionPolicy": {
"copilot": "committed_projection",
"claude": "generated_local"
}
}

Gated Drift Detection Per Target

Require clean sync for production targets but allow drift in development:

{
"ciPolicy": {
"requireCleanSync": true
}
}

Then in CI:

wl sync --check --target api
# Fails if api target has drift (production)

wl sync --check --target experiment
# (experiment target can have drift; not checked in CI)

Troubleshooting

"Package not found in target X"

A workspace includes a package that doesn't specify your target. Check the workspace manifest:

wl status --target my-target
# Error: Package @acme/baseline@1.0.0 does not support target my-target

Add the target path to the package's target list or ensure the target's path matches the package's expectations.

"Files in wrong location"

A file renders to an unexpected path. Check the asset's declared path:

{
"path": ".claude/settings.json" // Relative to target path
}

And the target:

{
"id": "myapp",
"path": "apps/myapp"
}

Result: apps/myapp/.claude/settings.json

"Drift in one target affects others"

Targets are independent—drift in one target doesn't affect others. Run wl status --target <id> to isolate the issue.

CI/CD Integration

In your monorepo CI, check all targets:

# Check all targets
wl sync --check

# Check specific target
wl sync --check --target frontend

# Check multiple targets
wl sync --check --target frontend --target api

Use with --locked for deterministic checks:

wl sync --check --locked
# Only apply locked versions, fail on any drift

Next Steps