For a pnpm workspace driven by Turborepo, with several deployable apps and shared internal packages. It assumes TypeScript project references, a shared UI package, and per-package scripts that Turbo orchestrates. Use it when the main risk is an agent editing the wrong package or breaking the dependency graph.
# AGENTS.md
This is a pnpm workspace managed by Turborepo containing several deployable apps and shared internal packages.
## Project
Stack: pnpm workspaces, Turborepo, TypeScript, React, ESLint, Vitest.
```
apps/
web/ public marketing site and app shell
admin/ internal admin dashboard
api/ HTTP service, deployed separately
packages/
ui/ shared React components, no app imports
core/ domain types and pure business logic
db/ schema, client, and query helpers
config/ shared eslint, tsconfig, and tailwind presets
```
Every package is private except where `publishConfig` says otherwise. Package names use the `@repo/` prefix.
Entry points: `turbo.json` (task graph), `pnpm-workspace.yaml` (package discovery), `packages/config/` (every shared preset).
## Architecture
Dependency direction is one way and enforced in review:
```
apps/* -> packages/ui, packages/core, packages/db
packages/ui -> packages/core
packages/db -> packages/core
packages/core -> nothing internal
```
Rules that follow from this:
- A package in `packages/` never imports from `apps/`.
- `packages/core` has no React, no database driver, and no runtime environment assumptions.
- Cross app imports are forbidden. If `admin` needs something from `web`, it moves to a package.
- Shared code is added to an existing package before a new package is created. Ask before adding a package.
- Internal packages are consumed from TypeScript source, not from a build artifact.
## Commands
Use pnpm at the workspace root.
```bash
pnpm install # install the whole workspace
pnpm dev # turbo run dev across apps
pnpm build # turbo run build, respects the task graph
pnpm lint
pnpm typecheck
pnpm test
pnpm --filter @repo/ui build # one package
pnpm --filter web dev # one app
pnpm --filter @repo/core test # one package's tests
pnpm --filter web add zod # add a dependency to one package only
turbo run build --filter=web... # a package and everything it depends on
```
Turbo caches task output. If a task reports success instantly it may be cached, use `--force` when verifying a fix.
Never run `pnpm add` at the root for a dependency that belongs to one package.
## Code style
- Each package exports through its `src/index.ts`. Deep imports into another package's internals are not allowed.
- `packages/ui` components take props only. No data fetching, no environment variables, no router imports.
- Shared packages have no top level side effects, importing them must be free.
- Types shared across packages live in `packages/core`, not duplicated per app.
- Extend the presets in `packages/config` rather than adding a local eslint or tsconfig rule.
- Keep one version of React, TypeScript, and every shared runtime dependency across the workspace.
- No default exports in shared packages, named exports keep refactors mechanical.
## Boundaries
Do not touch without explicit instruction:
- `turbo.json`, root `package.json`, `pnpm-workspace.yaml`.
- `pnpm-lock.yaml`. Use `pnpm --filter <pkg> add` and let it update.
- `packages/config/` presets, they change behavior for every package at once.
- `.github/workflows/` and any deploy configuration.
- Package `name`, `version`, or `exports` fields.
Needs human review: adding a new package, adding a dependency between packages, upgrading a shared runtime dependency, and anything that changes the public surface of `packages/core`.
## Testing
- `pnpm test` runs everything, `pnpm --filter @repo/core test` runs one package.
- `packages/core` and `packages/db` require tests for every exported function.
- `packages/ui` requires tests only for components with logic, not for layout wrappers.
- Apps require tests for route handlers and any code that is not a thin composition of packages.
- When changing a shared package, run the tests of every consumer: `turbo run test --filter=...@repo/core`.
## Git workflow
- Branch from `main`: `feat/short-description`, `fix/short-description`.
- Conventional commits with the package as scope: `fix(ui): correct focus ring on icon button`.
- A commit that touches several packages is fine when it is one logical change, otherwise split it.
- Any change to a package that is published requires a changeset: `pnpm changeset`.
- PR description lists which packages changed and whether consumers need updating.
- Never push to `main`.
In a monorepo the first question an agent gets wrong is which package a file belongs to. Listing apps and packages with a one-line purpose each turns that into a lookup. It also makes it obvious when a change is being made in the wrong layer.
The dependency direction between packages is the rule that keeps a monorepo from collapsing into a tangle, and it is invisible from any single file. Writing it down stops an agent from adding an import that creates a cycle or pulls app code into a shared package. This is the section that pays for itself fastest.
Turbo commands run across the graph, so the difference between a root command and a filtered one is significant for both speed and blast radius. Showing the filter syntax means the agent works on one package instead of rebuilding everything. Include the cache behavior so it does not misread a cached success as a real one.
Shared packages have stricter rules than apps because their mistakes multiply across every consumer. Rules about exports, side effects, and public surface are the ones that actually matter here. Formatting is handled by tooling and does not need to be described.
Workspace plumbing, the root package.json, turbo.json, and tsconfig bases, breaks everything at once when it is wrong. Version drift between packages is another silent failure an agent can introduce with a single install. Mark these as human territory.
Tests live per package, so the agent needs to know how to run just the affected ones rather than the whole repo. Shared packages need higher coverage than apps because their consumers depend on stable behavior. State which packages are non-negotiable.
Monorepo commits benefit from a scope that names the package, since a reviewer scanning history needs to know what was touched. Changesets or a similar versioning step is easy to forget and blocks release when missed. Say when a version bump is required.
Claude Code looks for CLAUDE.md. Most other agents and editors read AGENTS.md. Rather than maintaining both and letting them drift, keep one real file and symlink the other:
ln -s AGENTS.md CLAUDE.md
git add AGENTS.md CLAUDE.md Git stores the symlink, so it survives cloning on macOS and Linux. On Windows it needs developer mode or a stub file that references the real one. The full comparison is in CLAUDE.md vs AGENTS.md.
Next.js SaaS
App Router SaaS with Supabase auth, Postgres, and Stripe billing.
React Native / Expo
Expo Router app with typed navigation, native modules, and EAS builds.
SvelteKit App
SvelteKit app with server load functions, form actions, and Vitest.
Open Source Library
Published npm package with a stable API, changesets, and docs.