`.claude/agents/` was the source of truth, which made every role Claude-Code shaped. Adding a second client meant rewriting each role in that client's syntax and maintaining both copies — the drift this scaffold exists to prevent, one layer up. Roles and skills now live under `.agents/` and render into each registered client. `.claude/agents/`, `.claude/skills/` and `.codex/agents/` are generated; `scripts/sync-agent-integrations.py --check` fails on drift and belongs in CI. The role metadata is portable rather than vendor-named: `reasoning_tier` (deep/balanced/fast/vision), `capabilities`, `mutation`, `invocation`, and an optional `preload_skills`. A client manifest maps those to native syntax and must declare what it cannot express — `codex.yaml` declares `tier_policy: unsupported` and its adapters say so in the file, rather than the tier silently evaporating and leaving the repository to believe it was enforced. The port is behaviour-preserving where it should be and a fix where it should not. Every instruction body is byte-identical — the whole diff to `.claude/agents/` is 18 added lines and zero deletions. What changed is frontmatter that was missing: - four agents (`code-reviewer`, `tdd-guardian`, `dependency-audit`, `pr-creator`) declared no `tools:` and therefore inherited the ENTIRE tool pool, so three review-only agents could edit and write the code they were reviewing. All eight now declare capabilities explicitly. - the six read-only roles gain a non-editing permission mode, so the constraint is enforced by the client rather than by the prompt asking nicely. - `mutation` is now explicit, which records the two roles that genuinely need to write: `pr-creator` (external-write — it pushes a branch and opens a PR) and `dependency-audit` (workspace-write — package managers rewrite lockfiles). `pr-creator` keeps `shell` because opening a PR needs it, but it is now the only agent here with a write mutation and a declared reason for it, instead of one of four with unlimited access by omission.
405 lines
9.4 KiB
Markdown
405 lines
9.4 KiB
Markdown
---
|
|
name: monorepo-patterns
|
|
description: Monorepo workspace patterns for multi-package projects with shared dependencies, testing strategies, and CI/CD. Use when working in monorepo structures.
|
|
---
|
|
|
|
# Monorepo Patterns Skill
|
|
|
|
## Recommended Structure
|
|
|
|
```
|
|
project/
|
|
├── apps/
|
|
│ ├── backend/ # Python FastAPI
|
|
│ │ ├── src/
|
|
│ │ ├── tests/
|
|
│ │ └── pyproject.toml
|
|
│ └── frontend/ # React TypeScript
|
|
│ ├── src/
|
|
│ ├── tests/
|
|
│ └── package.json
|
|
├── packages/
|
|
│ ├── shared-types/ # Shared TypeScript types
|
|
│ │ ├── src/
|
|
│ │ └── package.json
|
|
│ └── ui-components/ # Shared React components
|
|
│ ├── src/
|
|
│ └── package.json
|
|
├── infrastructure/
|
|
│ ├── terraform/
|
|
│ │ ├── environments/
|
|
│ │ └── modules/
|
|
│ └── ansible/
|
|
│ ├── playbooks/
|
|
│ └── roles/
|
|
├── scripts/ # Shared scripts
|
|
├── docs/ # Documentation
|
|
├── .github/
|
|
│ └── workflows/
|
|
├── package.json # Root (workspaces config)
|
|
├── pyproject.toml # Python workspace config
|
|
└── CLAUDE.md # Project-level guidance
|
|
```
|
|
|
|
## Workspace Configuration
|
|
|
|
### npm Workspaces (Node.js)
|
|
```json
|
|
// package.json (root)
|
|
{
|
|
"name": "my-monorepo",
|
|
"private": true,
|
|
"workspaces": [
|
|
"apps/*",
|
|
"packages/*"
|
|
],
|
|
"scripts": {
|
|
"dev": "npm run dev --workspaces --if-present",
|
|
"build": "npm run build --workspaces --if-present",
|
|
"test": "npm run test --workspaces --if-present",
|
|
"lint": "npm run lint --workspaces --if-present",
|
|
"typecheck": "npm run typecheck --workspaces --if-present"
|
|
},
|
|
"devDependencies": {
|
|
"typescript": "^5.6.0",
|
|
"vitest": "^3.2.0",
|
|
"@types/node": "^22.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### UV Workspace (Python)
|
|
```toml
|
|
# pyproject.toml (root)
|
|
[project]
|
|
name = "my-monorepo"
|
|
version = "0.0.0"
|
|
requires-python = ">=3.11"
|
|
|
|
[tool.uv.workspace]
|
|
members = ["apps/*", "packages/*"]
|
|
|
|
[tool.uv.sources]
|
|
shared-utils = { workspace = true }
|
|
```
|
|
|
|
## Package References
|
|
|
|
### TypeScript Internal Packages
|
|
```json
|
|
// packages/shared-types/package.json
|
|
{
|
|
"name": "@myorg/shared-types",
|
|
"version": "0.0.0",
|
|
"private": true,
|
|
"main": "./dist/index.js",
|
|
"types": "./dist/index.d.ts",
|
|
"exports": {
|
|
".": {
|
|
"types": "./dist/index.d.ts",
|
|
"import": "./dist/index.js"
|
|
}
|
|
},
|
|
"scripts": {
|
|
"build": "tsc",
|
|
"dev": "tsc --watch"
|
|
}
|
|
}
|
|
|
|
// apps/frontend/package.json
|
|
{
|
|
"name": "@myorg/frontend",
|
|
"dependencies": {
|
|
"@myorg/shared-types": "workspace:*"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Python Internal Packages
|
|
```toml
|
|
# packages/shared-utils/pyproject.toml
|
|
[project]
|
|
name = "shared-utils"
|
|
version = "0.1.0"
|
|
dependencies = []
|
|
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
|
|
# apps/backend/pyproject.toml
|
|
[project]
|
|
name = "backend"
|
|
dependencies = [
|
|
"shared-utils", # Resolved via workspace
|
|
]
|
|
```
|
|
|
|
## Testing Strategies
|
|
|
|
### Run All Tests
|
|
```bash
|
|
# From root
|
|
npm test # All Node packages
|
|
uv run pytest # All Python packages
|
|
|
|
# Specific workspace
|
|
npm test --workspace=@myorg/frontend
|
|
uv run pytest apps/backend/
|
|
```
|
|
|
|
### Test Dependencies Between Packages
|
|
```typescript
|
|
// packages/shared-types/src/user.ts
|
|
export type User = {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
};
|
|
|
|
// apps/frontend/src/features/users/types.ts
|
|
// Import from workspace package
|
|
import type { User } from '@myorg/shared-types';
|
|
|
|
export type UserListProps = {
|
|
users: User[];
|
|
onSelect: (user: User) => void;
|
|
};
|
|
```
|
|
|
|
### Integration Tests Across Packages
|
|
```typescript
|
|
// apps/frontend/tests/integration/api.test.ts
|
|
import { User } from '@myorg/shared-types';
|
|
import { renderWithProviders } from '../utils/render';
|
|
|
|
describe('Frontend-Backend Integration', () => {
|
|
it('should display user from API', async () => {
|
|
const mockUser: User = {
|
|
id: 'user-1',
|
|
email: '[email protected]',
|
|
name: 'Test User',
|
|
};
|
|
|
|
// Mock API response with shared type
|
|
server.use(
|
|
http.get('/api/users/user-1', () => HttpResponse.json(mockUser))
|
|
);
|
|
|
|
render(<UserProfile userId="user-1" />);
|
|
|
|
await expect(screen.findByText('Test User')).resolves.toBeInTheDocument();
|
|
});
|
|
});
|
|
```
|
|
|
|
## CI/CD Patterns
|
|
|
|
### Change Detection
|
|
```yaml
|
|
# .github/workflows/ci.yml
|
|
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
detect-changes:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
frontend: ${{ steps.changes.outputs.frontend }}
|
|
backend: ${{ steps.changes.outputs.backend }}
|
|
infrastructure: ${{ steps.changes.outputs.infrastructure }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: dorny/paths-filter@v3
|
|
id: changes
|
|
with:
|
|
filters: |
|
|
frontend:
|
|
- 'apps/frontend/**'
|
|
- 'packages/shared-types/**'
|
|
- 'packages/ui-components/**'
|
|
backend:
|
|
- 'apps/backend/**'
|
|
- 'packages/shared-utils/**'
|
|
infrastructure:
|
|
- 'infrastructure/**'
|
|
|
|
frontend:
|
|
needs: detect-changes
|
|
if: needs.detect-changes.outputs.frontend == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
cache: 'npm'
|
|
- run: npm ci
|
|
- run: npm run typecheck --workspace=@myorg/frontend
|
|
- run: npm run lint --workspace=@myorg/frontend
|
|
- run: npm run test --workspace=@myorg/frontend
|
|
|
|
backend:
|
|
needs: detect-changes
|
|
if: needs.detect-changes.outputs.backend == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: astral-sh/setup-uv@v4
|
|
- run: uv sync
|
|
- run: uv run ruff check apps/backend/
|
|
- run: uv run mypy apps/backend/
|
|
- run: uv run pytest apps/backend/ --cov --cov-fail-under=80
|
|
```
|
|
|
|
### Jenkinsfile for Monorepo
|
|
```groovy
|
|
// Jenkinsfile
|
|
pipeline {
|
|
agent any
|
|
|
|
stages {
|
|
stage('Detect Changes') {
|
|
steps {
|
|
script {
|
|
def changes = sh(
|
|
script: 'git diff --name-only HEAD~1',
|
|
returnStdout: true
|
|
).trim().split('\n')
|
|
|
|
env.FRONTEND_CHANGED = changes.any { it.startsWith('apps/frontend/') || it.startsWith('packages/') }
|
|
env.BACKEND_CHANGED = changes.any { it.startsWith('apps/backend/') }
|
|
env.INFRA_CHANGED = changes.any { it.startsWith('infrastructure/') }
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('Frontend') {
|
|
when {
|
|
expression { env.FRONTEND_CHANGED == 'true' }
|
|
}
|
|
steps {
|
|
dir('apps/frontend') {
|
|
sh 'npm ci'
|
|
sh 'npm run typecheck'
|
|
sh 'npm run lint'
|
|
sh 'npm run test'
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('Backend') {
|
|
when {
|
|
expression { env.BACKEND_CHANGED == 'true' }
|
|
}
|
|
steps {
|
|
sh 'uv sync'
|
|
sh 'uv run ruff check apps/backend/'
|
|
sh 'uv run pytest apps/backend/ --cov --cov-fail-under=80'
|
|
}
|
|
}
|
|
|
|
stage('Infrastructure') {
|
|
when {
|
|
expression { env.INFRA_CHANGED == 'true' }
|
|
}
|
|
steps {
|
|
dir('infrastructure/terraform') {
|
|
sh 'terraform init'
|
|
sh 'terraform validate'
|
|
sh 'terraform fmt -check -recursive'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Dependency Management
|
|
|
|
### Shared Dependencies at Root
|
|
```json
|
|
// package.json (root)
|
|
{
|
|
"devDependencies": {
|
|
// Shared dev dependencies
|
|
"typescript": "^5.6.0",
|
|
"vitest": "^3.2.0",
|
|
"eslint": "^9.0.0",
|
|
"@types/node": "^22.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Package-Specific Dependencies
|
|
```json
|
|
// apps/frontend/package.json
|
|
{
|
|
"dependencies": {
|
|
// App-specific dependencies
|
|
"react": "^18.3.0",
|
|
"@tanstack/react-query": "^5.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Commands Quick Reference
|
|
|
|
```bash
|
|
# Install all dependencies
|
|
npm install # Node (from root)
|
|
uv sync # Python
|
|
|
|
# Run in specific workspace
|
|
npm run dev --workspace=@myorg/frontend
|
|
npm run test --workspace=@myorg/shared-types
|
|
|
|
# Run in all workspaces
|
|
npm run build --workspaces
|
|
npm run test --workspaces --if-present
|
|
|
|
# Add dependency to specific package
|
|
npm install lodash --workspace=@myorg/frontend
|
|
uv add requests --package backend
|
|
|
|
# Add shared dependency to root
|
|
npm install -D prettier
|
|
```
|
|
|
|
## CLAUDE.md Placement
|
|
|
|
### Root CLAUDE.md (Project-Wide)
|
|
```markdown
|
|
# Project Standards
|
|
|
|
[Core standards that apply everywhere]
|
|
```
|
|
|
|
### Package-Specific CLAUDE.md
|
|
```markdown
|
|
# apps/frontend/CLAUDE.md
|
|
|
|
## Frontend-Specific Standards
|
|
|
|
- Use React Testing Library for component tests
|
|
- Prefer Radix UI primitives
|
|
- Use TanStack Query for server state
|
|
```
|
|
|
|
```markdown
|
|
# apps/backend/CLAUDE.md
|
|
|
|
## Backend-Specific Standards
|
|
|
|
- Use pytest-asyncio for async tests
|
|
- Pydantic v2 for all schemas
|
|
- SQLAlchemy 2.0 async patterns
|
|
```
|
|
|
|
Skills in `~/.claude/skills/` are automatically available across all packages in the monorepo.
|