# AGENTS.md — operating rules for this repository ## 1. Read this order, every time 1. `.agents/PLAN.md` — master design. Its **Architectural Anchors** table holds the **LOCKED DECISIONS**, which are binding on every task and phase. 2. This file. 3. `.agents/phases/todo//00_phase.md`, then the task file you are executing. 4. The user story referenced by the phase (`.agents/user_stories/US-XX_*.md`). 5. `.agents/phases/complete/*/00_phase.md` for what already exists (do not re-build it). ## 2. The phased protocol - Phase state lives in files, not chat. Pending work: `.agents/phases/todo/NN_name/` (`00_phase.md` + `NN_task.md`). Finished work: `.agents/phases/complete/`. - Execution is owned by the `phased-execution` skill (`run-task.sh`, `run-phase.sh`, `auto-phase.sh`). Each task runs in a fresh subprocess and must pass `.agents/validate.sh` before it moves to `complete/`. That gate fails fast in this order: `ruff check .` → `pyright` → `undercooked-levels validate` (unconditional: the same loader + PLAN §8 rules `create_app` runs at startup, over `$UNDERCOOKED_LEVELS_DIR`) → the **full** `pytest` suite with the coverage floor → the slow sim stage (`pytest -m slow tests/sim`: the replay goldens and the perf smoke, named on their own) → repo-hygiene checks. It runs from the repo root or from `.agents/`. - New work is authored by the `phase-authoring` skill as a phase directory — not implemented straight from chat. - **Never** modify `.agents/PLAN.md` or anything under `.agents/phases/complete/`. - **Ask the user** before editing an existing file in `.agents/phases/todo/`. - Do not renumber or reuse phase/task numbers. ## 3. LOCKED DECISIONS (summary — the table in PLAN.md is authoritative) - **Python 3.12+ / uv / FastAPI / native WebSockets** (`uvicorn[standard]`). No Socket.IO, no SSE, no WebRTC, no REST for live game traffic. - **Server-authoritative fixed-tick simulation** (20 Hz) with client interpolation. The browser never decides anything true. `src/undercooked/sim/` is pure: **no FastAPI, no asyncio, no I/O, no wall-clock, no `random` without the round RNG**. - **No database, no ORM, no Alembic.** Live state lives in the room supervisor; results/scoreboards live in memory and die with the process. User-approved deviation from the default FastAPI stack. - **Data-driven content.** Levels/recipes/hazards come from YAML in `levels/` loaded through validated Pydantic models. No hard-coded level or recipe lists in Python. `create_app()` loads the bundle once (`routes_content.load_validated_content`) and **refuses to start** when content fails to load or violates a PLAN §8 rule — the named-rule report goes to stderr, `ContentError` propagates and the `undercooked` command exits `2` like it does for an impossible setting. The content API projects through `extra="forbid"` response models: grids, station ids and spawn coordinates never leave the server. `undercooked-levels` (`tools/levels.py`, its author CLI) obeys the same rule: it prints whatever the loaded directory holds. `tests/unit/test_content_not_hardcoded.py` is the lock — it adds a level YAML to a copy of `levels/` and expects the loader, `/api/levels` and the CLI to carry it with no Python edited, and fails if any `src/undercooked/**.py` or browser asset names a shipped level or recipe id as a literal. - **Client: Canvas 2D + vanilla ES modules + this repo's own SVG art.** No bundler, no framework, no CDN, no analytics, no external fonts. - **Tests: pytest + Playwright.** One Playwright suite per user story; multi-player stories use two browser contexts against one room. - Quality gate: ruff + pyright + full pytest, **>90% coverage** on new/modified code, **entire suite 100% passing** before a phase may complete or commit. ## 4. Definition of done (every task) - [ ] Task's own work exists at the named paths and is importable/runnable. - [ ] Tests written for the task's logic (unit for pure logic, integration for rooms/protocol, e2e for user-visible flows) and passing. - [ ] **Full** project suite passes (`uv run pytest`), coverage gate met — including failures the task did not cause: fix them, do not skip, xfail, or delete tests. - [ ] `uv run ruff check .` and `uv run pyright` report zero errors. - [ ] `uv run undercooked-levels validate` passes — `.agents/validate.sh` runs it for every task, whether or not any content file changed. - [ ] `README.md` and `AGENTS.md` updated when the change is user-visible (setup, commands, config, protocol, features, conventions). Every phase's final pass re-reviews both **in full, project-wide** and fixes inconsistencies, including pre-existing ones. - [ ] App still launches: `uv run undercooked` serves `http://127.0.0.1:8000/healthz` with `200` and the client at `/`. - [ ] No behavior regression in any completed phase. ## 5. Hard rules - **Configuration is read once, in `config.py`.** `src/undercooked/config.py` is the only module that touches `os.environ`. To add a knob: add a `Settings` field + `ENV_KEYS` entry + a line in `.env.example` + a row in the README table (a test fails when those disagree), and read it from `get_settings()`. Values are validated at startup: an impossible one raises `ConfigError` rather than half-configuring the server (`undercooked` prints one line and exits `2`). Precedence is **CLI flag > environment > built-in default**: `__main__.py` resolves it purely, then hands the flags to `config.apply_cli_overrides()`, which is the only writer of `os.environ` (so a uvicorn `--reload` child re-reads the same values). `DEBUGPY=1` binds a debugger listener on `0.0.0.0` — dev-only, never on a public host. - **No external CDNs or remote assets.** `tests/unit/test_no_external_assets.py` fails the build on `http://`/`https://`/`//host` references, `@import url(`, `srcset`, an absolute-URL `fetch()`/`import`, an absolute-URL `WebSocket`, or `EventSource` anywhere in `app/static/**` (html/css/js/svg), and requires every `