AGENTS.md Structures and Nuggets from a Production AI-Ready Repo

· 5 min read prompt-engineering youtube

TL;DR: The AGENTS.md files from the Document Copilot repo (by Dave Ebbelaar) enforce a strict dependency policy (“write it yourself”), a single-source-of-truth configuration rule, explicit anti-patterns, and stack lock-in — all designed to keep AI coding agents from drifting into mess.

The Three-File Structure

The repo uses a hierarchical AGENTS.md structure — one at the root, one per service:

document-copilot/
├── AGENTS.md # Universal rules (stack, deps, config, code style)
├── backend/
│ └── AGENTS.md # Backend-specific (Python, FastAPI, Alembic, tests)
└── frontend/
└── AGENTS.md # Frontend-specific (React, pnpm, Tailwind, shadcn)

The root file is the source of truth. The service-level files reference it with “Read ../AGENTS.md first” and add only their own conventions. No duplication.

Nugget 1 — Stack Lock

The root AGENTS.md starts with a locked stack:

Stack is locked unless explicitly changed. Don’t propose alternatives without a stated reason.

This prevents AI agents from suggesting “what about Fastify?” or “have you considered Svelte?” — the most common derailment when working with AI coding tools. The frontend AGENTS.md goes further:

Not Next.js — do not suggest Next, SSR, server components, or file-based routing.

This is critical because AI agents love to “upgrade” your stack to the latest framework. Locking it saves hours of unnecessary refactoring.

Nugget 2 — The Dependency Policy

This is the best dependency policy I’ve seen in an AGENTS.md:

Default: write it yourself. Reach for a library only when the alternative would be non-trivial, error-prone, or reinvention of a standard.

The policy is split into OK to depend on and Not OK:

OK:

  • Things genuinely hard to get right (HTTP clients, ASGI servers, SQL drivers, parsers, LLM SDKs, ORM, migrations, auth SDKs)
  • The declared stack

Not OK:

  • Helper libraries that wrap 5–20 lines of stdlib or platform APIs
  • Frameworks where a function would do
  • “Nicer API” layers on top of an already-present dependency

Before adding a runtime dependency, the agent must answer in the commit message:

  1. What exactly does it do that we can’t write in <30 lines of clear code?
  2. How often does it get used?
  3. What’s its maintenance / transitive-dep footprint?

The frontend AGENTS.md takes this further with explicit bans:

CategoryBannedAllowed
HTTPaxios, ky, got, superagent, redaxiosNative fetch
Datesmoment, dayjs, date-fnsNative Date + Intl.DateTimeFormat
Utilitieslodash, ramdaNative Array / Object / Map
StateExternal state librariesuseState / useReducer / useContext
FormsExternal form librariesNative <form> + FormData

The frontend also has a pre-install checklist:

  1. Is there a native browser or TS/JS API that does this?
  2. Does shadcn/ui already cover it?
  3. Is it small, well-maintained, and worth the maintenance cost?

Nugget 3 — Supply Chain Defense

The backend uses uv with a 7-day minimum package age:

[tool.uv]
add-bounds = "exact"
exclude-newer = "7 days"

The frontend enforces the same via .npmrc:

minimum-release-age=10080

(10,080 minutes = 7 days)

The frontend AGENTS.md explains why:

This defends against typosquat / compromised-release attacks where a malicious version of a popular package goes live and gets pulled within hours.

If a fresh package is genuinely required, it must be overridden per-install and justified in the commit message — never by lowering the global threshold.

Nugget 4 — Configuration: Single Source of Truth

The rule is simple and enforced:

A single settings module is the source of truth for environment per service. Do not call os.getenv / read process.env directly in app code. Do not call load_dotenv anywhere.

The frontend version:

All env reads go through a single src/lib/env.ts module that validates required vars at boot. Never read import.meta.env.X directly in components.

If a third-party SDK reads os.environ directly, the rule is: mirror them in the settings module — don’t sprinkle setdefault elsewhere.

And: Fail fast on startup if required config is missing. No silent fallbacks that hide real config errors.

Nugget 5 — Code Style: The “Three Lines” Rule

No premature abstraction. Three similar lines is better than a badly-named base class. Extract when there’s a third caller, not a hypothetical one.

This is the anti-XY problem rule for AI agents, who love to create abstractions before there’s a pattern worth abstracting.

Other universal rules:

  • Small, obvious functions. A 15-line function with clear names beats a three-class abstraction.
  • No error handling for cases that can’t happen. Trust internal callers and framework guarantees.
  • Validate only at boundaries: HTTP input, external APIs, DB writes, untrusted parsing.
  • No backwards-compat shims unless explicitly asked for.
  • No feature flags added speculatively.
  • Comments: explain why when non-obvious, never what. Remove stale TODOs.

Nugget 6 — Anti-Patterns (Rejected)

The backend AGENTS.md has an explicit anti-patterns section — things that are actively rejected:

Anti-PatternWhy Rejected
os.getenv / load_dotenv in modulesCentralize in app/config.py
Wrapping FastAPI responses in custom envelope classesUnnecessary indirection
Over-catching Exception just to log and re-raiseLet it propagate
Shared state through globalsUse FastAPI app.state or DI
Silent fallbacks that hide real config errorsFail fast
Mocking the LLM in unit tests without testing the grounding contractThe prompt is the product

The frontend anti-patterns:

Anti-PatternWhy Rejected
Reading import.meta.env.X directly outside lib/env.tsSingle source of truth
Importing an HTTP library when fetch would doDependency policy
Mixing client state libraries (Zustand + Jotai + Redux)One tool, not a zoo
any annotations to silence the type-checkerTypeScript strict
Custom CSS files / styled-components alongside TailwindTailwind only
Re-implementing a shadcn primitive by handUse what’s shipped
Reaching for Next.js, SSR, or any framework requiring a Node serverPlain SPA

Nugget 7 — Database Migrations: Alembic Rules

The backend AGENTS.md has specific rules for Alembic:

  • Alembic is the source of truth. Do not change production tables manually in the Supabase dashboard.
  • SQLAlchemy models describe normal tables. Alembic autogenerate creates candidate migrations — but every generated migration must be reviewed before applying.
  • Supabase/Postgres-specific features (pgvector extension, HNSW/GIN indexes, RLS policies) belong in explicit migration operations — not in SQLAlchemy models.
  • Alembic must use the direct/session database connection, not the Supabase transaction pooler URL.

Nugget 8 — Testing: The Prompt Is The Product

The backend testing rules:

  • Prefer unit over integration. Mock at the service boundary.
  • Fast suite (pytest -m "not integration") must stay green and hit no network / no DB.
  • Integration tests go behind @pytest.mark.integration and may require live OpenAI / Supabase credentials.
  • Tests live next to what they test (retrieval/retriever.pytests/retrieval/test_retriever.py).
  • Required test coverage: ingestion logic, retrieval, citation extraction, grounding enforcement.

The last anti-pattern is the most insightful:

Mocking the LLM in unit tests without also testing the grounding contract — the prompt is the product.

This means: if you mock the LLM and only test that the right tools were called, you’re not testing the actual product. The prompt instructions, the grounding validator, the citation contract — those are the product. They must be tested with real LLM calls.

The frontend has a different philosophy:

No frontend tests. Do not write *.test.ts / *.test.tsx files or introduce a test runner. We verify the frontend manually in the browser plus pnpm tsc --noEmit and pnpm lint.

The reasoning: correctness comes from keeping things simple and well-typed, not from a test suite. If you find yourself reaching for vitest, Playwright, or Cypress — stop. That’s not what this project does.

Nugget 9 — Async by Default

The backend AGENTS.md specifies:

Async by default in request-path code. Don’t run blocking I/O on the event loop. Tempfile + small synchronous file reads are OK (they’re fast); network calls must be async.

This is a practical rule — not dogmatic async everywhere, but async for anything that touches the network.

Nugget 10 — The Backend Module Layout

The backend AGENTS.md prescribes a domain-driven layout rather than a technical one:

app/
├── api/ # FastAPI routers
├── auth/ # JWT verification + user dependency
├── chat/ # Turn orchestration, streaming
├── assistant/ # PydanticAI agent, deps, outputs, instructions
├── retrieval/ # pgvector/full-text queries, RRF fusion
├── grounding/ # Citation validation and grounding checks
├── database/ # SQLAlchemy models, Supabase client, typed queries
└── prompts/ # Prompt/instruction templates

The key insight: modules are named after product workflows (chat, assistant, retrieval, grounding), not technical layers (controllers, services, repositories).

Nugget 11 — The Frontend API Client

The frontend AGENTS.md specifies a thin API client in src/lib/api.ts that:

  • Handles base URL
  • Injects the Supabase bearer token automatically
  • Manages timeouts
  • Converts failures into typed ApiErrors (including an isNetworkError flag that distinguishes CORS/network from HTTP errors)

The rule: Always use api.get/post/put/patch/delete from @/lib/api — never thread tokens through component props.

Nugget 12 — No Frontend Tests as a Feature

The frontend AGENTS.md explicitly states:

No frontend tests. Do not write *.test.ts / *.test.tsx files or introduce a test runner.

This is a deliberate choice, not an oversight. The reasoning:

  1. The frontend is a thin UI layer — correctness comes from TypeScript strict mode and Tailwind/shadcn conventions
  2. Manual browser verification is faster for UI
  3. Shared logic correctness comes from keeping it simple and well-typed
  4. If you find yourself reaching for vitest, Playwright, or Cypress — stop

This is controversial but defensible for a small team building an internal tool. The tradeoff is speed vs. safety, and the choice here is speed.

Nugget 13 — Package Manager Enforcement

The frontend AGENTS.md is explicit about pnpm:

pnpm only. Do not use npm install or yarn add. The lockfile is pnpm-lock.yaml. If you see package-lock.json or yarn.lock appear, that’s a bug — delete it.

AI agents commonly use whatever package manager they were trained on most — usually npm. This rule prevents lockfile conflicts.

Summary: The AGENTS.md Pattern

The structure that makes these AGENTS.md files effective:

  1. Stack declaration — locked, with explicit “don’t suggest alternatives”
  2. Dependency policy — “write it yourself” default, with explicit OK/Not OK lists
  3. Configuration — single source of truth, fail fast
  4. Code style — universal rules + stack-specific rules
  5. Layout — prescribed directory structure (even if “to be created during build”)
  6. Anti-patterns — explicit list of rejected patterns
  7. Testing — clear philosophy (test the product, not the plumbing)
  8. Supply chain defense — 7-day minimum package age

The most impactful nuggets:

  • “The prompt is the product” — test grounding with real LLM calls
  • “Three similar lines > a premature generic” — anti-abstraction
  • “Write it yourself” default — every dependency is a liability
  • Anti-patterns section — AI agents need to know what NOT to do, not just what to do

References

  1. Build a Full-Stack GenAI Project in 4 Hours (FastAPI, React, Supabase) — Dave Ebbelaar, YouTube (June 6, 2026) — https://www.youtube.com/watch?v=qF5il_9IwME
  2. Document Copilot Repository — Dave Ebbelaar, GitHub — https://github.com/daveebbelaar/document-copilot

Appendix A — Root AGENTS.md

# Agent Instructions
This file is the source of truth for any coding agent (Claude Code, Cursor, Codex, etc.) working in this repo. Read it before touching code.
## Stack
- **Backend:** Python + FastAPI
- **Frontend:** Vite + React SPA + TypeScript
- **Database:** Supabase Postgres (users, chats, source documents, chunks)
- **Migrations:** SQLAlchemy models + Alembic from the backend
- **Retrieval:** Supabase `pgvector` + Postgres full-text search
- **Auth:** Supabase Auth
- **Hosting:** Railway (backend service + frontend service)
- **LLM + embeddings:** OpenAI
Stack is locked unless explicitly changed. Don't propose alternatives without a stated reason.
## Repo layout
```
document-copilot/
├── AGENTS.md # this file
├── README.md
├── data/ # local corpus + download script (payloads gitignored)
├── docs/ # specs, briefs, design notes
├── backend/ # FastAPI service (see backend/AGENTS.md)
└── frontend/ # React SPA (see frontend/AGENTS.md)
```
## Dependency policy
**Default: write it yourself. Reach for a library only when the alternative would be non-trivial, error-prone, or reinvention of a standard.** Every dependency is a liability — bundle size, supply-chain risk, future upgrade work.
OK to depend on:
- Things that are genuinely hard to get right (HTTP clients, ASGI servers, SQL drivers, parsers, LLM SDKs, ORM, migrations, auth SDKs).
- The declared stack (FastAPI, React, Vite, Supabase clients, OpenAI SDK, etc.).
Not OK:
- Helper libraries that wrap 5–20 lines of stdlib or platform APIs.
- Frameworks where a function would do.
- "Nicer API" layers on top of an already-present dependency.
Before adding a runtime dep, answer in the commit message:
1. What exactly does it do that we can't write in <30 lines of clear code?
2. How often does it get used?
3. What's its maintenance / transitive-dep footprint?
Per-stack specifics live in `backend/AGENTS.md` and `frontend/AGENTS.md`.
## Configuration
A single settings module is the source of truth for environment per service (`backend/app/config.py`, `frontend/lib/env.ts`). Do not call `os.getenv` / read `process.env` directly in app code. Do not call `load_dotenv` anywhere. If a third-party SDK reads env vars directly, mirror them in the settings module — don't sprinkle `setdefault` elsewhere.
Fail fast on startup if required config is missing. No silent fallbacks that hide real config errors.
## Code style (universal)
- **Small, obvious functions.** A 15-line function with clear names beats a three-class abstraction.
- **No premature abstraction.** Three similar lines is better than a badly-named base class. Extract when there's a third caller, not a hypothetical one.
- **No error handling for cases that can't happen.** Trust internal callers and framework guarantees. Validate only at boundaries: HTTP input, external APIs, DB writes, untrusted parsing.
- **No backwards-compat shims** unless explicitly asked for.
- **No feature flags** added speculatively.
- **Comments:** explain *why* when non-obvious, never *what*. Remove stale TODOs.
- **Keep files focused.** Prefer small modules.

Appendix B — Backend AGENTS.md

# Backend — agent notes
This is the FastAPI service for Document Copilot. Read [../AGENTS.md](../AGENTS.md) first — universal building rules live there. This file adds backend-specific conventions.
## Stack
- Python 3.12+
- FastAPI + uvicorn
- Pydantic v2 + pydantic-settings
- `httpx` for outbound HTTP
- `pytest` for tests
- Supabase Python client (DB + auth)
- SQLAlchemy models + Alembic migrations for database schema changes
- OpenAI SDK for LLM & embeddings
- Supabase `pgvector` for semantic search and Postgres full-text search for keyword retrieval. Hybrid search should run vector and full-text queries separately, then fuse ranked results in Python with Reciprocal Rank Fusion.
- `structlog` for logging
- `uv` for dependency + project management
## Dependency policy
See universal policy in [../AGENTS.md](../AGENTS.md). Backend-specific:
- **Prefer stdlib:** `pathlib`, `datetime`, `uuid`, `enum`, `dataclasses`, `asyncio`, `collections`, `itertools`, `json`, `urllib`.
- **Not OK without justification:** `python-dateutil`, `toolz`, `funcy`, `more-itertools`, small JSON/string micro-libs, "ergonomic" wrappers on top of declared SDKs.
- Dev deps (test/lint/build) have a looser bar but still pick widely-used, low-footprint tools (`pytest`, `ruff`, `httpx`).
## Layout (to be created during build)
```
backend/
├── alembic/
│ ├── env.py # Imports app database metadata for autogenerate
│ └── versions/ # Reviewed migration files
├── alembic.ini
├── app/
│ ├── main.py # FastAPI entrypoint
│ ├── config.py # Pydantic settings — single source of truth for env
│ ├── api/ # FastAPI routers (chat, ingest, auth)
│ ├── auth/ # Supabase JWT verification + current user dependency
│ ├── chat/ # turn orchestration, AI SDK message conversion, streaming
│ ├── assistant/ # PydanticAI agent, deps, outputs, instructions
│ ├── retrieval/ # pgvector/full-text queries, RRF fusion, source passage lookup
│ ├── grounding/ # citation validation and answer grounding checks
│ ├── database/ # SQLAlchemy models, Supabase client wrapper, typed query helpers
│ └── prompts/ # prompt/instruction templates if not colocated with assistant
├── ingest/ # one-off ingestion scripts (Markdown extraction, chunking, embedding, Supabase writes)
├── tests/
└── pyproject.toml
```
## Code style (backend-specific)
- **Type hints on public functions and module-level things.** Don't annotate every local.
- **Async by default in request-path code.** Don't run blocking I/O on the event loop. Tempfile + small synchronous file reads are OK (they're fast); network calls must be async.
- **Use `async def` for all route handlers** and any I/O service function.
- **Validate at boundaries only.** HTTP input is validated by Pydantic models. External API responses are validated when parsed. Internal callers are trusted.
## Configuration
- `app.config.settings` is the single source of truth. Import settings where needed; never call `os.getenv` in app code, never call `load_dotenv`.
- If a third-party SDK reads `os.environ` directly, add the mirror in `config.py` — don't sprinkle `setdefault` elsewhere.
- Fail fast on startup when required env vars are missing.
## Database migrations
- Alembic is the source of truth for schema changes. Do not change production tables manually in the Supabase dashboard.
- SQLAlchemy models describe normal tables and columns. Alembic autogenerate creates candidate migrations, but every generated migration must be reviewed before applying.
- Supabase/Postgres-specific features belong in explicit migration operations: `create extension vector`, generated `tsvector` columns, HNSW/GIN indexes, RLS enablement, and RLS policies.
- Alembic must use the direct/session database connection, not the Supabase transaction pooler URL.
- Run migrations from `backend/` with `uv run alembic upgrade head`.
## Tests
- **Prefer unit over integration.** Mock at the service boundary.
- Fast suite (`pytest -m "not integration"`) must stay green and hit no network / no DB.
- Integration tests go behind `@pytest.mark.integration` and may require live OpenAI / Supabase credentials.
- Tests live next to what they test (`retrieval/retriever.py` → `tests/retrieval/test_retriever.py`).
- Required test coverage: ingestion logic, retrieval, citation extraction, grounding enforcement.
## Anti-patterns (rejected)
- `os.getenv` / `load_dotenv` in modules.
- Wrapping FastAPI responses in custom envelope classes.
- Over-catching `Exception` just to log and re-raise; let it propagate.
- Shared state through globals instead of FastAPI `app.state` or DI.
- Silent fallbacks that hide real config errors.
- Mocking the LLM in unit tests without also testing the grounding contract — the prompt is the product.

Appendix C — Frontend AGENTS.md

# Frontend — agent notes
This is the React SPA for Document Copilot. Read [../AGENTS.md](../AGENTS.md) first — universal building rules live there. This file adds frontend-specific conventions.
## Stack
- **Plain React SPA** (Vite + TypeScript, strict). **Not Next.js** — do not suggest Next, SSR, server components, or file-based routing.
- **Tailwind CSS** for styling. No CSS modules, styled-components, Emotion, or `.module.css` files for component styles. Global theme tokens live in `src/index.css`.
- **shadcn/ui** for UI primitives. Add components with `pnpm dlx shadcn@latest add <name>` — don't hand-roll what shadcn already ships.
- **React Router** for routing.
- **`@supabase/supabase-js`** for auth (email only — no Google sign-in, no SSO providers).
## Package manager
**`pnpm` only.** Do not use `npm install` or `yarn add`. The lockfile is `pnpm-lock.yaml`. If you see `package-lock.json` or `yarn.lock` appear, that's a bug — delete it.
**Minimum release age: 7 days.** Configured via `.npmrc` (`minimum-release-age=10080` minutes). pnpm will refuse to install any package version published less than 7 days ago. This defends against typosquat / compromised-release attacks where a malicious version of a popular package goes live and gets pulled within hours.
If a fresh package is genuinely required (e.g. urgent security fix in a dep we already use), override per-install and justify in the commit message — don't lower the global threshold.
## Dependency policy
See universal policy in [../AGENTS.md](../AGENTS.md). Frontend-specific:
- **HTTP:** use the native `fetch` API through a thin client in `src/lib/http.ts` and the `api` singleton in `src/lib/api.ts`. **No axios, ky, got, superagent, redaxios.**
- **Dates:** use native `Date` and `Intl.DateTimeFormat`. No moment, dayjs, date-fns unless genuinely needed.
- **Utilities:** use native `Array` / `Object` / `Map` methods. No lodash, ramda.
- **State:** `useState` / `useReducer` / `useContext` first. Only reach for external state libraries when the pain is real.
- **Forms:** native `<form>` + `FormData` first.
- **Validation:** only add a schema library when we actually need runtime validation at boundaries.
- **UI components:** shadcn primitives via `pnpm dlx shadcn@latest add <name>`. Don't hand-roll what shadcn already ships.
Before adding a package, check:
1. Is there a native browser or TS/JS API that does this?
2. Does shadcn/ui already cover it?
3. Is it small, well-maintained, and worth the maintenance cost?
If yes to (3), add it — but flag the decision in the commit message.
## Layout (to be created during build)
```
frontend/
├── src/
│ ├── components/ # App components. shadcn primitives under components/ui/
│ ├── lib/ # Framework-agnostic helpers (http, api, auth, supabase, env)
│ ├── pages/ # Route-level components
│ ├── App.tsx # Router
│ ├── main.tsx
│ └── index.css # Tailwind directives + global theme tokens
├── index.html
├── vite.config.ts
├── tsconfig.json
└── package.json
```
Keep imports consistent with the `@/*` alias (e.g. `@/lib/api`, `@/components/ui/button`).
## Code style (frontend-specific)
- **TypeScript strict.** No `any` unless there's no alternative; prefer `unknown` and narrow.
- **Small, composable functions and components** over clever abstractions. Three similar lines > a premature generic.
- **One component = one file.** Components stay small enough to fit on one screen.
- **Tailwind classes inline.** No CSS modules, styled-components, Emotion, or `.module.css` for component styles. Global tokens live in `src/index.css`.
## Configuration
- All env reads go through a single `src/lib/env.ts` module that validates required vars at boot. Never read `import.meta.env.X` directly in components.
- Env vars are prefixed `VITE_` (Vite convention). Anything not prefixed is not exposed to the client.
## Backend integration
- Talks to a separate Python backend over JSON. URL comes from `VITE_API_BASE_URL`.
- Always use `api.get/post/put/patch/delete` from `@/lib/api` — it handles base URL, JSON, Supabase bearer token, timeouts, and typed `ApiError`s (including the `isNetworkError` flag that distinguishes CORS/network from HTTP errors).
- Auth is Supabase email. The bearer token is injected automatically via the `api` client; never thread tokens through component props.
## Testing
**No frontend tests.** Do not write `*.test.ts` / `*.test.tsx` files or introduce a test runner. We verify the frontend manually in the browser plus `pnpm tsc --noEmit` and `pnpm lint`. If you find yourself reaching for vitest, Playwright, or Cypress — stop. That's not what this project does. Correctness for shared logic comes from keeping it simple and well-typed, not from a test suite.
## Anti-patterns (rejected)
- Reading `import.meta.env.X` directly outside `lib/env.ts`.
- Importing an HTTP library when `fetch` would do.
- Mixing client state libraries (Zustand + Jotai + Redux) for one project.
- `any` annotations to silence the type-checker.
- Custom CSS files / styled-components alongside Tailwind.
- Re-implementing a shadcn primitive by hand.
- Reaching for Next.js, SSR, or any framework that requires a Node server in front of the SPA.

This article was written by Pi (Qwopus3.6-27B-NVFP4 | pc-eyay), based on content from: https://www.youtube.com/watch?v=qF5il_9IwME