SKILL.md
$28
When to Use
- Before adding a second or third client to an existing project
- After a project has grown organically and the boundaries are fuzzy
- When you notice
if (client === 'acme')checks creeping into shared code
- Before a major refactor to understand what's actually shared vs specific
- When onboarding a new developer who needs to understand the architecture
- Periodic health check on multi-client projects
Modes
Mode
Trigger
What it produces
audit
"fork discipline", "check the boundary"
Boundary map + violation report
document
"write FORK.md", "document the boundary"
FORK.md file for the project
refactor
"clean up the fork", "enforce the boundary"
Refactoring plan + migration scripts
Default: audit
Audit Mode
Step 1: Detect Project Type
Determine if this is a multi-client project and what pattern it uses:
Signal
Pattern
clients/ or tenants/ directory
Explicit multi-client
Multiple config files with client names
Config-driven multi-client
packages/ with shared + per-client packages
Monorepo multi-client
Environment variables like CLIENT_NAME or TENANT_ID
Runtime multi-client
Only one deployment, no client dirs
Single-client (may be heading multi-client)
If single-client: check if the project CLAUDE.md or codebase suggests it will become multi-client. If so, audit for readiness. If genuinely single-client forever, this skill isn't needed.
Step 2: Map the Boundary
Build a boundary map by scanning the codebase:
CORE (shared by all clients):
src/server/ → API routes, middleware, auth
src/client/ → React components, hooks, pages
src/db/schema.ts → Shared database schema
migrations/0001-0050 → Core migrations
CLIENT (per-deployment):
clients/acme/config.ts → Client overrides
clients/acme/kb/ → Knowledge base articles
clients/acme/seed.sql → Seed data
migrations/0100+ → Client schema extensions
BLURRED (needs attention):
src/server/routes/acme-custom.ts → Client code in core!
src/config/defaults.ts line 47 → Hardcoded client domain
Step 3: Find Violations
Scan for these specific anti-patterns:
#### Client Names in Core Code
# Search for hardcoded client identifiers in shared code
grep -rn "acme\|smith\|client_name_here" src/ --include="*.ts" --include="*.tsx"
# Search for client-specific conditionals
grep -rn "if.*client.*===\|switch.*client\|case.*['\"]acme" src/ --include="*.ts" --include="*.tsx"
# Search for environment-based client checks in shared code
grep -rn "CLIENT_NAME\|TENANT_ID\|process.env.*CLIENT" src/ --include="*.ts" --include="*.tsx"
Severity: High. Every hardcoded client check in core code means the next client requires modifying shared code.
#### Config Replacement Instead of Merge
Check if client configs replace entire files or merge over defaults:
// BAD — client config is a complete replacement
// clients/acme/config.ts
export default {
theme: { primary: '#1E40AF' },
features: { emailOutbox: true },
// Missing all other defaults — they're lost
}
// GOOD — client config is a delta merged over defaults
// clients/acme/config.ts
export default {
theme: { primary: '#1E40AF' }, // Only overrides what's different
}
// config/defaults.ts has everything else
Look for: client config files that are suspiciously large (close to the size of the defaults file), or client configs that define fields the defaults already handle.
Severity: Medium. Stale client configs miss new defaults and features.
#### Scattered Client Code
Check if client-specific code lives outside the client directory:
# Files with client names in their path but inside src/
find src/ -name "*acme*" -o -name "*smith*" -o -name "*client-name*"
# Routes or pages that serve a single client
grep -rn "// only for\|// acme only\|// client-specific" src/ --include="*.ts" --include="*.tsx"
Severity: High. Client code in src/ means core is not truly shared.
#### Missing Extension Points
Check if core has mechanisms for client customisation without modification:
Extension point
How to check
What it enables
Config merge
Does config/ have a merge function?
Client overrides without replacing
Dynamic imports
Does core look for clients/{name}/custom/?
Client-specific routes/pages
Feature flags
Are features toggled by config, not code?
Enable/disable per client
Theme tokens
Are colours/styles in variables, not hardcoded?
Visual customisation
Content injection
Can clients provide seed data, templates?
Per-client content
Hook/event system
Can clients extend behaviour without patching?
Custom business logic
Severity: Medium. Missing extension points force client code into core.
#### Migration Number Conflicts
# List all migration files with their numbers
ls migrations/ | sort | head -20
# Check if client migrations are in the reserved ranges
# Core: 0001-0099, Client domain: 0100-0199, Client custom: 0200+
Severity: Low until it causes a conflict, then Critical.
#### Feature Flags vs Client Checks
// BAD — client name check
if (clientName === 'acme') {
showEmailOutbox = true;
}
// GOOD — feature flag in config
if (config.features.emailOutbox) {
showEmailOutbox = true;
}
Search for patterns where behaviour branches on client identity instead of configuration.
Step 4: Produce the Report
Write to .jez/artifacts/fork-discipline-audit.md:
# Fork Discipline Audit: [Project Name]
**Date**: YYYY-MM-DD
**Pattern**: [explicit multi-client / config-driven / monorepo / single-heading-multi]
**Clients**: [list of client deployments]
## Boundary Map
### Core (shared)
| Path | Purpose | Clean? |
|------|---------|--------|
| src/server/ | API routes | Yes / No — [issue] |
### Client (per-deployment)
| Client | Config | Content | Schema | Custom |
|--------|--------|---------|--------|--------|
| acme | config.ts | kb/ | 0100-0120 | custom/routes/ |
### Blurred (needs attention)
| Path | Problem | Suggested fix |
|------|---------|--------------|
| src/routes/acme-custom.ts | Client code in core | Move to clients/acme/custom/ |
## Violations
### High Severity
[List with file:line, description, fix]
### Medium Severity
[List with file:line, description, fix]
### Low Severity
[List]
## Extension Points
| Point | Present? | Notes |
|-------|----------|-------|
| Config merge | Yes/No | |
| Dynamic imports | Yes/No | |
| Feature flags | Yes/No | |
## Health Score
[1-10] — [explanation]
## Top 3 Recommendations
1. [Highest impact fix]
2. [Second priority]
3. [Third priority]
Document Mode
Generate a FORK.md for the project root that documents the boundary:
# Fork Discipline
## Architecture
This project serves multiple clients from a shared codebase.
### What's Core (don't modify per client)
[List of directories and their purpose]
### What's Client (varies per deployment)
[Client directory structure with explanation]
### How to Add a New Client
1. Copy `clients/_template/` to `clients/new-client/`
2. Edit `config.ts` with client overrides
3. Add seed data to `content/`
4. Create migrations numbered 0100+
5. Deploy with `CLIENT=new-client wrangler deploy`
### The Fork Test
Before modifying any file: is this core or client?
- Core → change in `src/`, all clients benefit
- Client → change in `clients/name/`, no other client affected
- Can't tell → the boundary needs fixing first
### Migration Numbering
| Range | Owner |
|-------|-------|
| 0001-0099 | Core platform |
| 0100-0199 | Client domain schema |
| 0200+ | Client custom features |
### Config Merge Pattern
Client configs are shallow-merged over defaults:
[Show the actual merge code from the project]
Refactor Mode
After an audit, generate the concrete steps to enforce the boundary:
1. Move Client Code Out of Core
For each violation where client code lives in src/:
# Create client directory if it doesn't exist
mkdir -p clients/acme/custom/routes
# Move the file
git mv src/routes/acme-custom.ts clients/acme/custom/routes/
# Update imports in core to use dynamic discovery
2. Replace Client Checks with Feature Flags
For each if (client === ...) in core:
// Before (in src/)
if (clientName === 'acme') {
app.route('/email-outbox', emailRoutes);
}
// After (in src/) — feature flag
if (config.features.emailOutbox) {
app.route('/email-outbox', emailRoutes);
}
// After (in clients/acme/config.ts) — client enables it
export default {
features: { emailOutbox: true }
}
3. Implement Config Merge
If the project replaces configs instead of merging:
// config/resolve.ts
import defaults from './defaults';
export function resolveConfig(clientConfig: Partial<Config>): Config {
return {
...defaults,
...clientConfig,
features: { ...defaults.features, ...clientConfig.features },
theme: { ...defaults.theme, ...clientConfig.theme },
};
}
4. Add Extension Point for Custom Routes
If clients need custom routes but currently modify core:
// src/server/index.ts — auto-discover client routes
const clientRoutes = await import(`../../clients/${clientName}/custom/routes`)
.catch(() => null);
if (clientRoutes?.default) {
app.route('/custom', clientRoutes.default);
}
5. Generate the Refactoring Script
Write a script to .jez/scripts/fork-refactor.sh that:
- Creates the client directory structure
- Moves identified files
- Updates import paths
- Generates the FORK.md
The Right Time to Run This
Client count
What to do
1
Don't refactor. Just document the boundary (FORK.md) so you know where it is.
2
Run the audit. Fix high-severity violations. Start the config merge pattern.
3+
Full refactor mode. The boundary must be clean — you now have proof of what varies.
Rule 5 from the discipline: Don't abstract until client #3. With 1 client you're guessing. With 2 you're pattern-matching. With 3+ you know what actually varies.
Tips
- Run this before adding a new client, not after
- Config merge is the single highest-ROI refactor — do it first
- Feature flags beat
if (client)even with one client. "This is mostly the same except..." = feature flag, not fork.
- FORK.md is for the team, not just for Claude — write it like a human will read it