Imported from regybean/Trellis (
AGENTS.md). Install upstream withnpx skills add regybean/Trellis. Copyright stays with the author.
AGENTS.md
This file provides guidance to coding agents working in this repository. It is the single source of truth: CLAUDE.md (Claude Code) and .github/copilot-instructions.md (Copilot) are symlinks to it, and Codex reads it directly. We highly encourage the autonomous use of the inbuilt skills in the repo, they will help you do your work correctly.
Navigation protocol — read before you search or edit
Consult the map before grepping; it turns most searches into a direct jump.
- Locating code in a feature/package? Don't grep first. Read its
CONTEXT-MAP.md row → the package's
CONTEXT.md, then docs/agents/feature-anatomy.md for the exact layout (api/routers/*,hooks/use-*,api/schemas/*). Jump to the file. - Hunting a symbol across packages? Split by operation.
LSPhover/workspaceSymbol/documentSymboland same-packagegoToDefinition/findReferencesare reliable — use them. But cross-packagegoToDefinition/findReferencesare NOT reliable here: packages consume each other through thedist/*.d.tsbarrel (exportstypes→dist), so tsserver can't link a source symbol to consumers in other packages (referencesdoesn't fix it — TS treats this as working-as-intended, and the only fix trades away the fast dist-based build). For cross-package reference hunting, use ripgrep scoped topackages//apps/, or theExploresubagent. - Broad / cross-layer search (a seam wired app→feature→platform; all callers
of X): delegate to the
Exploresubagent so the fan-out stays out of this context. Single greps and known-location lookups stay inline. - After each file you write, tests included:
pnpm turbo run lint typecheck -F @acme/<pkg>(cached, seconds — catches boundaries/exports/as/useEffect, and type errors insrc/tests/**thatvitest runcannot see because it does not typecheck). Thenpnpm tidy(auto-fix), apnpm turbo run typecheckbecause autofix can change types, andpnpm quality-gate(read-only verify) once at end of task.
turbois not installed globally — always invoke it aspnpm turbo ….
Project Overview
This is a Turborepo monorepo RAG starter. The architecture enforces strict boundaries between layers using turbo.json and pnpm workspace configuration. The main motivation is to be able to maintain many different apps with forward feature compatibility, aka you update one feature and all the other apps immediately gain the improvements. This and all other codebase patterns are enforced with linting rules and other tooling, if you are doing something wrong the gates will flag it.
The repository works using vertical feature slices that are:
- Full stack feature slices with both BE and FE code
- Isolated between one another
- Define their own infrastructure
- Define their own testing infrastructure
- Define their own database schema and api router
- Define their own UI components and pages
- Can be implemented by any of the applications, whatever framework each is on
- Potentially composed using other packages
There is also a large focus on tooling, DDD and LLM HITL skills to improve the design process and to not let the LLM make architectural decisions without oversight.
Commands
Code Quality
pnpm lint # Run ESLint across all packages
pnpm lint:fix # Auto-fix linting issues
pnpm format # Check Prettier formatting
pnpm format:fix # Auto-fix formatting issues
pnpm lint:ws # Check workspace consistency with sherif
pnpm boundaries # Verify layer boundary violations
pnpm tidy # Auto-fix: lint:fix + format:fix (run before the gate)
Building and Testing
pnpm build # Build all packages
pnpm typecheck # Type check all packages
pnpm test # Run all tests via Vitest
pnpm test:backend # Backend tests only (real Postgres/Redis via testcontainers)
pnpm test:frontend # Frontend tests only (jsdom + MSW at HTTP boundary)
pnpm test:watch # Run tests in watch mode
pnpm turbo run test -F <pkg> # Run tests for a single package (turbo isn't global)
pnpm test:policy # Enforce per-package acme.testClass coverage
pnpm test:inventory [pkg|app...] # Markdown list of collected tests — an app expands to its closure; runs none, starts nothing
pnpm test:inventory -- --layer backend --kind unit # narrow by path segment under src/tests/ (--out <path> to write it)
Tests split into test:backend (real Postgres/Redis via testcontainers) and
test:frontend (jsdom + MSW at the HTTP boundary). Backend suites always
start their own throwaway containers — everywhere, identically — so the only
prerequisite is a reachable container runtime, never pnpm infra:up.
Doctrine, both halves: docs/TESTING.md, with the backend
taxonomy in docs/agents/testing.md. Rule of thumb:
test the contract, not the internals — the tRPC procedure on the backend, the
hook on the frontend; never vi.mock a seam the feature owns. Frontend: assert rendered DOM and hook state, never mock call counts; toasts go through <ToastContainer /> in the test wrapper, asserted through the DOM. Env is never mocked — tests run against real env values, never a faked shape. More information about testing can be found in testing.md
Infrastructure & Database
pnpm infra:up # Start local services — profile derived from acme.infra package metadata
pnpm infra:down # Stop services
pnpm infra:logs # Tail compose logs
pnpm with-env <cmd> # Run cmd with .env hydrated
pnpm db:push # Push schema changes, dev only (run)
pnpm preview [app...] # Serve the COMPILED build locally (no HMR) for true paint-time — same args/infra as dev; runs turbo `start` (dependsOn: build) instead of `watch dev`. Each app has its own port, printed on start.
Full Validation
pnpm tidy # Auto-fix first (lint:fix + format:fix) — the gate is read-only
pnpm quality-gate # READ-ONLY verify, parallel: build + turbo(lint+format+typecheck) + test + check:exports + check:imports + check:bank-paths + check:bank-tokens + check:adrs + check:portable + boundaries + lint:ws + deps:lint + test:policy + gitleaks
How and when to run these — incremental per-package checks and the end-of-task gate — is docs/agents/quality-gate.md, which also carries the rationale.
In a git worktree, dev/preview/infra/env/database commands are manual-only — do not run them. On the primary checkout (e.g.
main) you may run them to test. But for observing dev output: the human runspnpm dev; its dev-server + infra output is mirrored tologs/*.log. Read those instead of startingpnpm devyourself to watch output — see docs/agents/dev-logs.md. (This supersedes the "you may run [dev] to test" allowance for observing dev output only; it stays silent onpreview/build/testand doesn't banpnpm infra:up.) Tests are the exception:pnpm testworks in a worktree exactly as it does on the primary checkout — every backend suite self-provisions isolated testcontainers, so there is nothing to start and nothing special about a worktree.
Architecture
Layer boundaries
tooling → platform → shared → features → apps
- tooling: Shared configs (ESLint, Prettier, TypeScript, Tailwind, Vitest, test-utils, github). Depends on tooling only.
- platform: Runtime substrate — the rails features run on (logger, telemetry, redis, subscriptions, trpc, db, entitlements). Depends on platform and tooling.
- shared: Reusable primitives (ui, hooks, auth, rag, models). Depends on shared, platform, and tooling.
- features: Domain modules. Depends on shared, platform, and tooling only.
- apps: Applications. Depends on all layers; own their shell/chrome. There is no compositions layer — the boundary tag is
app.
Redis
All keys must be created via nsKey(key) from @acme/redis — produces a branded NamespacedKey type; passing a raw string is a compile error. Key builders (e.g. creditKey) live in domain packages, not in @acme/redis. Namespace is derived from NEXT_PUBLIC_WEBAPP.
Database
All app-owned tables live under pgSchema(NEXT_PUBLIC_WEBAPP) — per-app Postgres schema isolation. @acme/db exports the sole connection factory (createDb()); features import it rather than declaring their own DB env. Migrations are app-owned (db:push / db:migrate run from the app, not the platform).
Feature package structure
See docs/agents/feature-anatomy.md.
Package exports convention
Every runtime package (packages/platform|shared|features) has a
package.json exports map following a bounded, concern-driven convention,
enforced by tooling/repo-checks/src/exports.ts (hard-fails pnpm lint).
tooling/* config packages are out of scope.
Development Patterns
Adding a New Feature
- Run
pnpm turbo genand select the feature generator - Define tRPC context in
src/api/trpc.tswith db clients - Create routers in
src/api/routers/and aggregate insrc/api/root.ts - Export components, hooks, and TRPCProvider from
src/index.ts - Create React provider in
src/trpc/react.tsx - Add API route in app at
src/app/api/trpc/<name>/[trpc]/route.ts
Agent Skills
Skills are vendored into .agents/skills/ (committed; pinned by skills-lock.json) — one agent-agnostic source of truth for every harness, and the directory Codex reads directly. Claude only discovers a skill once it's symlinked into .claude/skills/; those symlinks are committed too (only .claude/worktrees/ is gitignored). scripts/register-skills.sh recreates them idempotently from .agents/skills/ and runs automatically on postinstall — run pnpm skills:register after adding or removing a skill, then commit the resulting link.
Each skill also carries agents/openai.yaml — its Codex-facing metadata (interface.display_name, interface.short_description, and policy.allow_implicit_invocation where the skill must not fire on its own). Keep it alongside the SKILL.md frontmatter when adding a skill; both describe the same skill to different harnesses.
Prose that reaches the user — grilling questions, plan and spec text, PR/issue bodies, end-of-task summaries — goes through the unslop skill before you send it.
Issue tracker
See docs/agents/issue-tracker.md.
Reads and writes against the tracker docs/agents/issue-tracker.md configures
are the normal operation of the tracker-driving workflows (wayfinding, triage,
spec-to-tickets), so they need no separate confirmation: assigning and
unassigning issues, adding and removing labels, commenting, and opening,
closing or editing issues and sub-issues. Nothing beyond that list, and in
particular no destructive Git operation, no permission or visibility change,
and no sending of repo contents to a third-party service.
Triage labels
See docs/agents/triage-labels.md.
Domain docs
Multi-context layout — CONTEXT-MAP.md at root points to per-package CONTEXT.md files. ADRs live with what they govern: repo-wide decisions in docs/adr/, a package's own in its docs/adr/, numbered per directory so the same number in both is normal. Placement, numbering, the status vocabulary and what a CONTEXT.md may hold are stated once in docs/agents/domain.md — don't restate them elsewhere. tooling/repo-checks/src/adrs.ts enforces what can be enforced, via pnpm lint.
Worktree workflow
See docs/agents/worktree-workflow.md.
Engineering direction
The north star, to weigh when making changes:
- Protect the slice contract. One feature = one package = router + hooks + UI, depending only downward. It's what lets apps mount different subsets — a bespoke client build is a new app importing a different subset, not a fork. Don't leak framework specifics into features; keep them in the app adapter (the honest seam).
- Keep seams swappable, name what's coupled. Providers (
@acme/models), auth (Better Auth behind a seam), billing (Stripe) are meant to be replaceable. When something becomes load-bearing or hard to reverse, write it down (ADR) rather than letting it harden silently. - Shell/chrome is app-owned. Framework-specific shell/chrome lives in the app; look at any app's console shell for the worked example. There is no compositions layer; shared UI assemblies go in
@acme/ui. A newpackages/compositions/entry requires an ADR justifying why the assembly can't live in an app or@acme/ui. - Earn the next runtime / the next subset. The portability and subsetting claims are only as true as the apps that prove them, so the app set is built to prove both: a full app and a reduced one on each framework it claims to support. The full pair proves the same slices run on two frameworks; the reduced pair proves a no-auth/no-billing subset drops the auth provider and Stripe from the graph. New shared/feature code must stay runtime-agnostic and not re-couple the substrate to auth/billing — design so the next framework or the next reduced subset stays trivial.
- Documentation keeps pace with design.
CONTEXT.md+ ADRs are updated as decisions are made (/grill-with-docs), not after. Keep the README honest — flag WIP/theoretical, never imply capabilities that don't exist.