Monorepo Tooling skill for Codex
Cache hits you can trust, CI that builds only what changed. Codex picks between plain pnpm workspaces, Turborepo and Nx based on repo size and needs, then declares each task's dependencies, inputs, outputs and environment variables so caching is both fast and correct. It proves the setup with a fully cached second run, a single-package change that rebuilds only dependents, and an env var change that forces a miss, and wires affected-only CI with full git history.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- monorepo-tooling Installs to ~/.agents/skills/monorepo-tooling/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install monorepo-tooling The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project monorepo-tooling Installs to .agents/skills/monorepo-tooling/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/monorepo-tooling.zipunzip -o monorepo-tooling.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) monorepo-tooling PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $monorepo-tooling, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Sets up and debugs JavaScript and TypeScript monorepo task running with pnpm workspaces, Turborepo and Nx - workspace protocol and catalogs, task graphs with dependsOn, declared inputs, outputs and environment variables so caching is correct, affected-only CI with full git history, remote caching and module boundaries. Use when creating or restructuring a monorepo, choosing between Turborepo and Nx, fixing stale or missing cache hits, making CI build only changed packages, or adding a new app or package to an existing workspace.
Use it when
- Creating a monorepo or adding apps and packages to one
- Builds are cached wrongly, restored empty, or never hit
- CI runs every task in every package on each PR
- Choosing between Turborepo, Nx and plain workspaces
Use something else when
- You need repo-level ownership and review routing per package: use codeowners-setup
- You are speeding up a single-package CI pipeline: use ci-speedup
- You are deciding how to split code into services or modules: use modular-monolith
How to check the result
A second full run is entirely cache hits, touching one leaf package rebuilds only it and its dependents, and changing a declared environment variable causes a cache miss for the tasks that use it.
The mistake it prevents
Leaving a build-time environment variable out of the task's env list, so a cached artifact built with another environment's value gets deployed.
Done well means: every task declares what it depends on, which files and environment variables change its result, and which files it produces. Then a second run with no changes is fully cached, a change in one package rebuilds only it and its dependents, and no cache hit ever serves an artifact built with different inputs. CI runs only affected work and stays correct.
Inspect
cat pnpm-workspace.yaml package.json turbo.json nx.json 2>/dev/null | head -120
ls apps packages libs 2>/dev/null
git ls-files '*package.json' | grep -v node_modules | wc -l
Read AGENTS.md for package manager and CI commands. Note the lockfile in use (only one
should exist) and how internal packages are consumed: compiled (dist), or TS source via
exports.
Choose the tool
| Situation | Use |
|---|---|
| Up to a handful of packages, simple scripts | pnpm workspaces alone: pnpm -r, --filter |
| JS/TS repo that wants caching and a task graph with minimal config | pnpm workspaces + Turborepo |
| Large org, many teams, generators, enforced boundaries, polyglot plugins | Nx (on pnpm/npm/yarn workspaces) |
| Already on one of these | Stay; fix its configuration rather than migrating |
pnpm workspaces
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
catalog:
react: ^19.1.0
typescript: ^5.9.0
// apps/web/package.json
{ "dependencies": { "@acme/ui": "workspace:*", "react": "catalog:" } }
pnpm install --frozen-lockfile # CI
pnpm --filter @acme/web... build # web and everything it depends on
pnpm --filter ...@acme/ui test # ui and everything that depends on it
pnpm --filter "...[origin/main]" test # packages changed since main, plus dependents
pnpm add -D -w typescript # root dev dependency
pnpm why react # duplicate versions
workspace:* is rewritten to the real version on publish. Catalogs keep one version of a
shared dependency across packages.
Turborepo
{
"$schema": "./node_modules/turbo/schema.json",
"globalDependencies": [".env.shared", "tsconfig.base.json"],
"globalEnv": ["NODE_ENV"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "!**/*.test.*", "!**/*.md"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NEXT_PUBLIC_*", "API_BASE_URL"]
},
"typecheck": { "dependsOn": ["^build"] },
"lint": {},
"test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] },
"dev": { "cache": false, "persistent": true }
}
}
^buildmeans “build of my dependencies first”;buildwithout the caret means a task in the same package;@acme/ui#buildtargets one package’s task.outputsis what gets restored on a cache hit. Missing outputs give a “cache hit” that restores nothing and a broken downstream build.inputsdefaults to all git-tracked files in the package;$TURBO_DEFAULT$keeps that default while you add exclusions.- Environment variables that affect output must be in
env(task) orglobalEnv. In strict env mode (the default in Turborepo 2), variables not listed are also hidden from the task at runtime; runtime-only variables that should not affect the hash go inpassThroughEnv.
turbo run build # whole graph
turbo run build --filter=@acme/web... # web plus its dependencies
turbo run test lint --affected # changed packages vs the default branch, plus dependents
turbo run build --dry=json | jq '.tasks[] | {taskId, cache: .cache.status}'
turbo run build --summarize # writes a run summary under .turbo/runs/
turbo run build --force # ignore cache (debugging only)
Remote cache: turbo login and turbo link for Vercel’s cache, or set TURBO_API,
TURBO_TOKEN and TURBO_TEAM for a self-hosted cache server. Linking an account or
writing tokens is an account change: ask the user first, and keep tokens in CI secrets.
Nx
{
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/*.spec.ts", "!{projectRoot}/**/*.md"],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production", { "env": "API_BASE_URL" }],
"outputs": ["{projectRoot}/dist"],
"cache": true
},
"test": { "inputs": ["default", "^production"], "cache": true }
}
}
npx nx graph # interactive project graph
npx nx show project web # resolved targets, including inferred ones
npx nx affected -t lint test build --base=origin/main --head=HEAD
npx nx run-many -t build --projects=tag:scope:shop
npx nx reset # clear local cache and daemon
Nx plugins infer targets from tool configs (Vite, Jest, Next.js); check nx show project
before adding explicit targets that duplicate them. Enforce dependency direction with the
@nx/enforce-module-boundaries ESLint rule and project tags (type:ui may not import
type:feature). Remote caching is Nx Cloud (nx connect, an account action that needs
approval) or a self-hosted cache.
CI
- uses: actions/checkout@v4
with:
fetch-depth: 0 # affected detection needs the merge base
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run lint typecheck test build --affected
Without a remote cache, persist .turbo/cache or .nx/cache with actions/cache keyed
on the lockfile hash and the commit SHA. For Nx on GitHub Actions, nrwl/nx-set-shas
computes the last successful base commit for affected.
Adding a package checklist
- [ ] Created under apps/ or packages/ matching the workspace globs
- [ ] name scoped (@acme/x), "private": true unless published
- [ ] Internal deps use workspace:*; shared versions use catalog:
- [ ] Scripts named like the rest (build, test, lint, typecheck)
- [ ] Outputs match the task's outputs globs; tsconfig extends the base
- [ ] Tags / boundaries set (Nx); CODEOWNERS entry added
- [ ] pnpm install updated the lockfile; affected run picks the package up
Gotchas
- An env var baked into a build (API URL, feature flag) but missing from
envproduces a cache hit with the wrong value in production. Auditprocess.env.usage per package. - Tasks that write outside their package directory, or read files not under it, break
hashing. Move shared inputs into
globalDependencies/sharedGlobals. - Non-deterministic outputs (timestamps, absolute paths) defeat remote cache sharing.
- Circular dependencies between packages make
^buildimpossible;pnpm ls -r --depth -1plusnx graphorturbo run build --graphreveals them. Break the cycle by extracting the shared part. - Phantom dependencies: code importing a package it does not list works with hoisting and fails elsewhere. pnpm’s isolated layout surfaces these; fix by declaring the dependency.
- Shallow clones make
--affectedeither fail or treat everything as changed. - Cached logs replay on hits; never print secrets in build output.
Verify
- Run the full task twice; the second run must be entirely cache hits.
- Touch one leaf package’s source;
turbo run build --affected --dry=jsonornx show projects --affectedlists only it and its dependents. - Change a declared env var; the affected task must miss the cache.
- Delete outputs locally and rerun; the cache must restore them.
Report: tool and version, task graph summary, cache hit rate before and after, CI time before and after, and any undeclared env vars or outputs found.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Monorepo Tooling"
short_description: "Cache hits you can trust, CI that builds only what changed"
default_prompt: "$monorepo-tooling Set up Turborepo in our pnpm monorepo so CI only builds and tests what changed" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.