mirror of
https://github.com/evennia/evennia.git
synced 2026-08-18 03:05:44 +00:00
Add agent scaffolding for those wanting to work in the Evennia repo
This commit is contained in:
47
.agents/docs/architecture.md
Normal file
47
.agents/docs/architecture.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Evennia Architecture
|
||||||
|
|
||||||
|
## Two-Process Model
|
||||||
|
|
||||||
|
Evennia runs as two cooperating Twisted processes:
|
||||||
|
|
||||||
|
- **Portal** (`server/portal/`) — faces the internet, handles all network protocols (telnet, SSH, SSL, websocket). Stays running during reloads.
|
||||||
|
- **Server** (`server/`) — runs game logic, Django ORM, commands. Can be reloaded without disconnecting players.
|
||||||
|
|
||||||
|
They communicate via an internal AMP (Asynchronous Messaging Protocol) connection.
|
||||||
|
|
||||||
|
## Typeclass System
|
||||||
|
|
||||||
|
The central abstraction. Every persistent game entity has two layers:
|
||||||
|
|
||||||
|
- **Database model** (e.g. `ObjectDB`, `AccountDB`, `ScriptDB`, `ChannelDB`) — Django model, stores data in the database.
|
||||||
|
- **Typeclass** (e.g. `DefaultObject`, `DefaultCharacter`, `DefaultRoom`, `DefaultExit`) — Python class that adds game logic, linked 1:1 to a DB model via `typeclass_path`.
|
||||||
|
|
||||||
|
Typeclasses live in `objects/`, `accounts/`, `scripts/`, `comms/`. The DB models are in `*/models.py`, typeclasses in `*/objects.py`, `*/accounts.py`, `*/scripts.py`, `*/comms.py`.
|
||||||
|
|
||||||
|
**Attributes and Tags** (`typeclasses/attributes.py`, `typeclasses/tags.py`) are the key-value and labeling systems stored on any typeclassed object. `AttributeProperty` and `TagProperty` allow declaring them as class-level descriptors.
|
||||||
|
|
||||||
|
## Command System
|
||||||
|
|
||||||
|
`commands/` — Commands are Python classes inheriting from `Command` (or `MuxCommand` for MUX-style parsing). They are grouped into `CmdSet` objects that merge/override each other on objects, accounts, and sessions.
|
||||||
|
|
||||||
|
Flow: input → `cmdhandler.py` (dispatch) → `cmdparser.py` (matching) → `Command.func()` execution.
|
||||||
|
|
||||||
|
Default commands are in `commands/default/` organized by category: `general.py`, `building.py`, `admin.py`, `comms.py`, `system.py`, `unloggedin.py`, `account.py`, `help.py`.
|
||||||
|
|
||||||
|
## Key Subsystems
|
||||||
|
|
||||||
|
- **Scripts** (`scripts/`) — Timed/persistent objects. Houses global handlers: `TICKER_HANDLER`, `MONITOR_HANDLER`, `TASK_HANDLER`, `ON_DEMAND_HANDLER`.
|
||||||
|
- **Locks** (`locks/`) — String-based permission system parsed at runtime. Lock functions in `lockfuncs.py`.
|
||||||
|
- **Help** (`help/`) — Database-backed help entries plus auto-generated help from command docstrings.
|
||||||
|
- **Prototypes** (`prototypes/`) — Dict-based templates for spawning objects via `evennia.spawn()`.
|
||||||
|
- **Web** (`web/`) — Django views, REST API (`web/api/`), webclient (`web/webclient/`), admin interface.
|
||||||
|
- **Utils** (`utils/`) — `EvMenu`, `EvTable`, `EvForm`, `EvEditor`, `EvMore`, `FuncParser`, `ANSIString`, `search_*` and `create_*` functions.
|
||||||
|
- **Contrib** (`contrib/`) — Community modules organized as `base_systems/`, `game_systems/`, `rpg/`, `grid/`, `tutorials/`, `utils/`, `full_systems/`.
|
||||||
|
|
||||||
|
## Flat API
|
||||||
|
|
||||||
|
`evennia/__init__.py` exposes a flat API — most important classes and functions are accessible as `evennia.DefaultObject`, `evennia.search_object()`, `evennia.create_object()`, etc. This API is lazy-loaded after Django initialization via `_init()`.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
`settings_default.py` (~1850 lines) is the master settings template. Game developers override specific values in their game dir's `server/conf/settings.py`. Never modify `settings_default.py` directly for a game — only when changing Evennia's own defaults.
|
||||||
53
.agents/docs/ci.md
Normal file
53
.agents/docs/ci.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# CI/CD
|
||||||
|
|
||||||
|
## Workflows (`.github/workflows/`)
|
||||||
|
|
||||||
|
### Test Suite (`github_action_test_suite.yml`)
|
||||||
|
|
||||||
|
Triggers on push/PR to `main` or `develop` (skips docs-only changes).
|
||||||
|
|
||||||
|
**Matrix**: Python 3.12, 3.13, 3.14 across three database jobs:
|
||||||
|
|
||||||
|
| Job | DB | Timeout | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| test-sqlite | SQLite | 30m | Coverage on Python 3.12 only |
|
||||||
|
| test-mysql | MySQL 8.0 | 35m | utf8mb3 charset, `--keepdb --parallel` |
|
||||||
|
| test-postgresql | PostgreSQL 14 | 60m | Sequential, no `--keepdb` (stale DB issues) |
|
||||||
|
|
||||||
|
**Deploy step** (main branch only, after all tests pass): builds and pushes Docker image `evennia/evennia:latest` to DockerHub.
|
||||||
|
|
||||||
|
### Doc Build (`github_action_build_docs.yml`)
|
||||||
|
|
||||||
|
Triggers on push/PR to `main`/`develop` when `docs/` or `evennia/contrib/` changes. Builds with Sphinx via `make release` in `docs/`. Requires a full game dir init + migrations before building.
|
||||||
|
|
||||||
|
### Other Workflows
|
||||||
|
|
||||||
|
- `codeql-analysis.yml` — Security scanning (JS + Python), weekly + on push/PR
|
||||||
|
- `github_action_issue_to_project.yml` — Auto-adds issues to GitHub Projects
|
||||||
|
|
||||||
|
## Custom Actions (`.github/actions/`)
|
||||||
|
|
||||||
|
- **`setup-database/`** — Waits for DB readiness, creates MySQL database/users, grants privileges. SQLite needs no setup.
|
||||||
|
- **`run-tests/`** — Sets up Python, installs Evennia, inits test game dir, copies DB-specific settings from `.github/workflows/{db}_settings.py`, runs migrations, then tests.
|
||||||
|
|
||||||
|
## Database Settings (`.github/workflows/`)
|
||||||
|
|
||||||
|
Each DB type has a settings file copied into the test game dir:
|
||||||
|
|
||||||
|
- `sqlite3_settings.py` — Minimal, uses Evennia defaults
|
||||||
|
- `mysql_settings.py` — utf8mb3 charset, `STRICT_TRANS_TABLES`, `innodb_strict_mode`
|
||||||
|
- `postgresql_settings.py` — Aggressive timeouts for CI: `lock_timeout=30s`, `statement_timeout=5m`
|
||||||
|
|
||||||
|
## Docker (`Dockerfile`)
|
||||||
|
|
||||||
|
- Base: `python:3.13-alpine`
|
||||||
|
- Build arg `EVENNIA_INSTALL_MODE`: `editable` (default, mounts source) or `pypi`
|
||||||
|
- Ports: 4000 (telnet), 4001 (web), 4002 (websocket)
|
||||||
|
- Volume: `/usr/src/game`
|
||||||
|
- Entrypoint: `bin/unix/evennia-docker-start.sh`
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
- `COVERALLS_REPO_TOKEN` — Coverage reporting
|
||||||
|
- `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` — Docker publishing
|
||||||
|
- `EVENNIA_TICKET_TO_PROJECT` — Issue automation
|
||||||
64
.agents/docs/code-style.md
Normal file
64
.agents/docs/code-style.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Code Style
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
Do not manually format code. Run `make format` (black + isort) after editing. It handles line length (100 chars), indentation, and import sorting. Use `make lint` to check without modifying.
|
||||||
|
|
||||||
|
## Docstrings
|
||||||
|
|
||||||
|
All modules, classes, functions, and methods must have docstrings. Use Google-style with Markdown formatting.
|
||||||
|
|
||||||
|
Import order (isort handles this, but be aware): stdlib → Twisted → Django → `evennia` → `evennia.contrib`
|
||||||
|
|
||||||
|
### Command Docstrings
|
||||||
|
|
||||||
|
Command class docstrings double as in-game help text. They use a special format with **2-space indentation**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Short header
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
key[/switches] <mandatory args> [optional]
|
||||||
|
|
||||||
|
Switches:
|
||||||
|
switch1 - description
|
||||||
|
switch2 - description
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
usage example and output
|
||||||
|
|
||||||
|
Longer documentation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
- `[ ]` for optional args, `< >` for descriptions of what to type, `||` to separate choices
|
||||||
|
- Commands requiring arguments should return a `Usage:` message when called with no args
|
||||||
|
|
||||||
|
### Function/Method Docstrings
|
||||||
|
|
||||||
|
Google-style with indented blocks:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def funcname(a, b, d=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Brief description.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a (str): Description over
|
||||||
|
multiple lines.
|
||||||
|
b (int or str): Another argument.
|
||||||
|
d (bool, optional): An optional keyword argument.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The result.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeException: If there is an error.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Additional context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
```
|
||||||
42
.agents/docs/commands.md
Normal file
42
.agents/docs/commands.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Development Commands
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Prefer `uv` over `pip` for faster installs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install -e . # or `make install`
|
||||||
|
uv pip install -e .[extra] # optional deps (crypto, SSL, Jupyter, scipy, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Game Lifecycle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
evennia --init mygame # create new game directory
|
||||||
|
cd mygame && evennia migrate
|
||||||
|
evennia start / stop / reload
|
||||||
|
evennia shell # Django-aware Python shell
|
||||||
|
evennia istart # interactive mode (for debugging with set_trace)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # full suite
|
||||||
|
make testp # parallel (4 cores)
|
||||||
|
make tests=evennia.objects.tests test # specific module
|
||||||
|
make tests=evennia.commands.tests.test_command test # specific test file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make format # black + isort
|
||||||
|
make lint # black --check
|
||||||
|
```
|
||||||
|
|
||||||
|
## PR Conventions
|
||||||
|
|
||||||
|
- Feature PRs and contribs go against the `develop` branch
|
||||||
|
- Critical fixes go against `main`
|
||||||
|
- Keep unrelated changes in separate branches/PRs
|
||||||
39
.agents/docs/core-beliefs.md
Normal file
39
.agents/docs/core-beliefs.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Core Beliefs
|
||||||
|
|
||||||
|
Design principles that inform implementation decisions in Evennia. When in doubt, these guide tradeoffs.
|
||||||
|
|
||||||
|
## Evennia is a toolkit, not a game
|
||||||
|
|
||||||
|
Evennia provides infrastructure (networking, persistence, command routing) without imposing genre, mechanics, or game style. Never add features that assume a specific type of game. Keep the core generic — game-specific systems belong in `contrib/` or downstream game code.
|
||||||
|
|
||||||
|
## Think in Python, not SQL
|
||||||
|
|
||||||
|
The typeclass system exists so developers work with Python classes, not database schemas. One `ObjectDB` table holds all objects; the `db_typeclass_path` field points to the Python class that gives it behavior. New entity types are created by subclassing in Python, not by adding database tables. Attributes (`db` handler) store arbitrary Python data without schema changes.
|
||||||
|
|
||||||
|
## Extend through hooks, not patches
|
||||||
|
|
||||||
|
Objects define clear hook methods called at specific lifecycle points (`at_object_creation`, `at_init`, `at_pre_move`, `at_look`, etc.). New behavior goes in hook overrides, not by modifying core internals. This keeps custom code predictable and upgrade-safe.
|
||||||
|
|
||||||
|
## Compose, don't branch
|
||||||
|
|
||||||
|
CommandSets merge using set operations (union, intersection, difference). Adding a CmdSet and then removing it restores the original state. This allows layering complex states (combat + darkness + status effects) without nested conditionals. Prefer composable, removable components over boolean flags.
|
||||||
|
|
||||||
|
## Fail closed
|
||||||
|
|
||||||
|
The lock system denies access by default. Everything is inaccessible unless explicitly permitted. When designing access checks, start locked and whitelist — don't start open and blacklist.
|
||||||
|
|
||||||
|
## Objects carry their own state
|
||||||
|
|
||||||
|
Handlers (Attributes, Tags, Locks, Scripts, Commands) attach directly to objects. State and behavior travel with the object, not in external registries. The idmapper cache ensures you always get the same Python instance for a given database object, so on-object state is reliable.
|
||||||
|
|
||||||
|
## Portal and Server are separate concerns
|
||||||
|
|
||||||
|
The Portal handles network protocols and stays running across reloads. The Server handles game logic and can be hot-reloaded. Neither knows the other's internals — they communicate via AMP. Don't leak protocol details into game logic or vice versa.
|
||||||
|
|
||||||
|
## The framework should be complete
|
||||||
|
|
||||||
|
Evennia includes its own web server, webclient, admin interface, and REST API. All connection methods (telnet, websocket, SSH) use the same game objects. Avoid requiring external services for core functionality.
|
||||||
|
|
||||||
|
## Keep the schema simple
|
||||||
|
|
||||||
|
Complexity grows through Python objects (typeclasses, attributes, tags), not through database tables. The core schema is intentionally minimal and stable. Resist adding new models — use Attributes and Tags on existing models instead when possible.
|
||||||
45
.agents/docs/testing.md
Normal file
45
.agents/docs/testing.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Testing
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Tests require a temporary game directory with migrations applied:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full test suite
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Parallel tests (4 cores)
|
||||||
|
make testp
|
||||||
|
|
||||||
|
# Specific module (TESTS variable)
|
||||||
|
make tests=evennia.objects.tests test
|
||||||
|
make tests=evennia.commands.tests.test_command test
|
||||||
|
|
||||||
|
# Manual equivalent (from inside a game dir)
|
||||||
|
evennia test --keepdb evennia.objects.tests
|
||||||
|
```
|
||||||
|
|
||||||
|
The Makefile creates a `.test_game_dir/`, runs `evennia migrate`, then `evennia test --keepdb`. Tests use Django's test runner, not pytest.
|
||||||
|
|
||||||
|
## Test Base Classes
|
||||||
|
|
||||||
|
All in `evennia/utils/test_resources.py`:
|
||||||
|
|
||||||
|
- **`BaseEvenniaTest`** — sets up default objects (account, char1, char2, room1, room2, obj1, obj2, exit, script, session) with enforced default settings. Use this for testing Evennia library code.
|
||||||
|
- **`BaseEvenniaCommandTest`** — extends `BaseEvenniaTest`, adds `.call(CmdClass, input, expected_output)` for testing command execution.
|
||||||
|
- **`EvenniaTest`** / **`EvenniaCommandTest`** — same but uses game-dir settings/typeclasses (for downstream game tests, not Evennia library tests).
|
||||||
|
- **`EvenniaTestCase`** — lightweight, no default objects created. Faster for tests that don't need game state.
|
||||||
|
|
||||||
|
## Agent Tooling Tests
|
||||||
|
|
||||||
|
Tests for `.agents/tools/` use pytest (not Django's runner). Run with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest .agents/tools/tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use bare `python -m pytest` — the system Python may not have pytest installed. `uv run` ensures the project venv is used.
|
||||||
|
|
||||||
|
## CI Matrix
|
||||||
|
|
||||||
|
CI tests against SQLite, MySQL 8.0, and PostgreSQL 14 across Python 3.12/3.13/3.14. Coverage is collected on Python 3.12 + SQLite only.
|
||||||
318
.agents/tools/clean_rot.py
Normal file
318
.agents/tools/clean_rot.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent context rot checker.
|
||||||
|
|
||||||
|
Validates that the agent knowledge base (AGENTS.md + .agents/docs/) stays lean,
|
||||||
|
cross-linked, and free of drift. Inspired by OpenAI's harness engineering approach:
|
||||||
|
AGENTS.md is a map (~100 lines), not a 1000-page manual. Detailed docs live in
|
||||||
|
.agents/docs/ and are only pulled in when needed.
|
||||||
|
|
||||||
|
Run from repo root:
|
||||||
|
python .agents/tools/clean_rot.py
|
||||||
|
|
||||||
|
Exit code 0 = clean, 1 = warnings found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# --- Thresholds ---
|
||||||
|
|
||||||
|
MAX_AGENTS_LINES = 40 # AGENTS.md is an index, not a manual
|
||||||
|
MAX_DOC_LINES = 120 # individual docs shouldn't bloat either
|
||||||
|
SIMILARITY_THRESHOLD = 0.6 # flag near-duplicate paragraphs between files
|
||||||
|
MIN_PARAGRAPH_LEN = 80 # ignore short lines for duplication checks
|
||||||
|
|
||||||
|
|
||||||
|
def _default_paths():
|
||||||
|
"""Return default paths derived from this script's location."""
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
return {
|
||||||
|
"repo_root": repo_root,
|
||||||
|
"agents_md": repo_root / "AGENTS.md",
|
||||||
|
"docs_dir": repo_root / ".agents" / "docs",
|
||||||
|
"src_dir": repo_root / "evennia",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def warn(category, msg):
|
||||||
|
"""Print a categorized warning."""
|
||||||
|
print(f" [{category}] {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_line_budget(agents_md, docs_dir, repo_root, **_kw):
|
||||||
|
"""AGENTS.md is the table of contents — keep it short."""
|
||||||
|
warnings = 0
|
||||||
|
|
||||||
|
if agents_md.exists():
|
||||||
|
lines = agents_md.read_text().splitlines()
|
||||||
|
count = len(lines)
|
||||||
|
if count > MAX_AGENTS_LINES:
|
||||||
|
warn(
|
||||||
|
"BLOAT",
|
||||||
|
f"AGENTS.md is {count} lines (budget: {MAX_AGENTS_LINES}). "
|
||||||
|
f"Move detail into .agents/docs/ and keep AGENTS.md as an index.",
|
||||||
|
)
|
||||||
|
warnings += 1
|
||||||
|
else:
|
||||||
|
warn("MISSING", "AGENTS.md not found.")
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
if docs_dir.exists():
|
||||||
|
for doc in sorted(docs_dir.glob("*.md")):
|
||||||
|
lines = doc.read_text().splitlines()
|
||||||
|
count = len(lines)
|
||||||
|
if count > MAX_DOC_LINES:
|
||||||
|
warn(
|
||||||
|
"BLOAT",
|
||||||
|
f"{doc.relative_to(repo_root)} is {count} lines "
|
||||||
|
f"(budget: {MAX_DOC_LINES}). Consider splitting.",
|
||||||
|
)
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
def check_broken_links(agents_md, docs_dir, repo_root, **_kw):
|
||||||
|
"""Verify all markdown links in AGENTS.md and .agents/docs/ point to files that exist."""
|
||||||
|
warnings = 0
|
||||||
|
if not agents_md.exists():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
text = agents_md.read_text()
|
||||||
|
# Match markdown links: [text](path) — skip http(s) URLs
|
||||||
|
for match in re.finditer(r"\[([^\]]+)\]\(([^)]+)\)", text):
|
||||||
|
label, target = match.group(1), match.group(2)
|
||||||
|
if target.startswith(("http://", "https://", "#")):
|
||||||
|
continue
|
||||||
|
resolved = repo_root / target
|
||||||
|
if not resolved.exists():
|
||||||
|
warn("BROKEN_LINK", f"AGENTS.md links to '{target}' ({label}) — file not found.")
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
# Also check links inside .agents/docs/
|
||||||
|
if docs_dir.exists():
|
||||||
|
for doc in docs_dir.glob("*.md"):
|
||||||
|
doc_text = doc.read_text()
|
||||||
|
for match in re.finditer(r"\[([^\]]+)\]\(([^)]+)\)", doc_text):
|
||||||
|
label, target = match.group(1), match.group(2)
|
||||||
|
if target.startswith(("http://", "https://", "#")):
|
||||||
|
continue
|
||||||
|
resolved = doc.parent / target
|
||||||
|
if not resolved.exists():
|
||||||
|
rel = doc.relative_to(repo_root)
|
||||||
|
warn("BROKEN_LINK", f"{rel} links to '{target}' ({label}) — file not found.")
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
def check_orphan_docs(agents_md, docs_dir, repo_root, **_kw):
|
||||||
|
"""Every file in .agents/docs/ should be referenced from AGENTS.md."""
|
||||||
|
warnings = 0
|
||||||
|
if not docs_dir.exists() or not agents_md.exists():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
agents_text = agents_md.read_text()
|
||||||
|
for doc in sorted(docs_dir.glob("*.md")):
|
||||||
|
rel_path = str(doc.relative_to(repo_root))
|
||||||
|
# Check both with and without leading ./
|
||||||
|
if rel_path not in agents_text and f"./{rel_path}" not in agents_text:
|
||||||
|
warn(
|
||||||
|
"ORPHAN",
|
||||||
|
f"{rel_path} is not referenced from AGENTS.md — "
|
||||||
|
f"agents won't discover it via progressive disclosure.",
|
||||||
|
)
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_paragraphs(text):
|
||||||
|
"""Split text into non-trivial paragraphs for duplication checking."""
|
||||||
|
paragraphs = []
|
||||||
|
current = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
if current:
|
||||||
|
para = " ".join(current)
|
||||||
|
if len(para) >= MIN_PARAGRAPH_LEN:
|
||||||
|
paragraphs.append(para)
|
||||||
|
current = []
|
||||||
|
else:
|
||||||
|
# Skip code blocks and headings
|
||||||
|
if not stripped.startswith(("```", "#", "- ", "| ")):
|
||||||
|
current.append(stripped)
|
||||||
|
if current:
|
||||||
|
para = " ".join(current)
|
||||||
|
if len(para) >= MIN_PARAGRAPH_LEN:
|
||||||
|
paragraphs.append(para)
|
||||||
|
return paragraphs
|
||||||
|
|
||||||
|
|
||||||
|
def check_duplication(agents_md, docs_dir, repo_root, **_kw):
|
||||||
|
"""Flag near-duplicate content between AGENTS.md and .agents/docs/ files.
|
||||||
|
|
||||||
|
Duplication means AGENTS.md is inlining detail instead of pointing to it.
|
||||||
|
"""
|
||||||
|
warnings = 0
|
||||||
|
if not agents_md.exists() or not docs_dir.exists():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
agents_paragraphs = _extract_paragraphs(agents_md.read_text())
|
||||||
|
|
||||||
|
for doc in sorted(docs_dir.glob("*.md")):
|
||||||
|
doc_paragraphs = _extract_paragraphs(doc.read_text())
|
||||||
|
rel = doc.relative_to(repo_root)
|
||||||
|
|
||||||
|
for ap in agents_paragraphs:
|
||||||
|
for dp in doc_paragraphs:
|
||||||
|
ratio = SequenceMatcher(None, ap, dp).ratio()
|
||||||
|
if ratio >= SIMILARITY_THRESHOLD:
|
||||||
|
snippet = ap[:80] + "..." if len(ap) > 80 else ap
|
||||||
|
warn(
|
||||||
|
"DUPLICATION",
|
||||||
|
f"AGENTS.md duplicates content from {rel} "
|
||||||
|
f"({ratio:.0%} similar): \"{snippet}\"",
|
||||||
|
)
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
def check_stale_references(agents_md, docs_dir, repo_root, src_dir, **_kw):
|
||||||
|
"""Check that source paths and Python module paths mentioned in docs still exist.
|
||||||
|
|
||||||
|
Only flags references that look like intentional source tree paths (must contain
|
||||||
|
a slash for file paths, or start with 'evennia.' for dotted module paths). Bare
|
||||||
|
filenames, CLI tools, branch names, and Python identifiers are ignored.
|
||||||
|
"""
|
||||||
|
warnings = 0
|
||||||
|
|
||||||
|
all_docs = []
|
||||||
|
if agents_md.exists():
|
||||||
|
all_docs.append(agents_md)
|
||||||
|
if docs_dir.exists():
|
||||||
|
all_docs.extend(docs_dir.glob("*.md"))
|
||||||
|
|
||||||
|
for doc in all_docs:
|
||||||
|
text = doc.read_text()
|
||||||
|
rel = doc.relative_to(repo_root)
|
||||||
|
|
||||||
|
# Check backtick-quoted paths that contain a slash (real source paths).
|
||||||
|
# e.g. `commands/default/`, `typeclasses/attributes.py`, `server/conf/settings.py`
|
||||||
|
# Skips bare filenames like `cmdhandler.py` and tools like `uv`.
|
||||||
|
for match in re.finditer(
|
||||||
|
r"`((?:evennia/)?[a-z_]+/[a-z_/]*(?:\.py)?/?)`", text
|
||||||
|
):
|
||||||
|
path_ref = match.group(1)
|
||||||
|
# Search broadly: repo root, src dir, src/contrib,
|
||||||
|
# game_template (for game-dir paths like server/conf/settings.py)
|
||||||
|
candidates = [
|
||||||
|
src_dir / path_ref,
|
||||||
|
repo_root / path_ref,
|
||||||
|
src_dir / "contrib" / path_ref,
|
||||||
|
src_dir / "game_template" / path_ref,
|
||||||
|
]
|
||||||
|
if path_ref.startswith("evennia/"):
|
||||||
|
candidates.append(repo_root / path_ref)
|
||||||
|
if not any(c.exists() for c in candidates):
|
||||||
|
warn("STALE_REF", f"{rel} references `{path_ref}` — path not found in source.")
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
# Check backtick-quoted Python dotted paths like `evennia.objects.tests`
|
||||||
|
for match in re.finditer(r"`(evennia\.[a-z_.]+)`", text):
|
||||||
|
dotted = match.group(1)
|
||||||
|
parts = dotted.split(".")
|
||||||
|
as_file = repo_root / Path(*parts).with_suffix(".py")
|
||||||
|
as_dir = repo_root / Path(*parts)
|
||||||
|
as_init = as_dir / "__init__.py"
|
||||||
|
if not (as_file.exists() or as_dir.exists() or as_init.exists()):
|
||||||
|
warn("STALE_REF", f"{rel} references `{dotted}` — module not found.")
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
def check_index_density(agents_md, **_kw):
|
||||||
|
"""AGENTS.md should be mostly pointers, not prose. Check the ratio of
|
||||||
|
link/reference lines vs content lines."""
|
||||||
|
warnings = 0
|
||||||
|
if not agents_md.exists():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
text = agents_md.read_text()
|
||||||
|
lines = text.splitlines()
|
||||||
|
non_empty = [l for l in lines if l.strip()]
|
||||||
|
if not non_empty:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Count lines that are structural: headings, links, bullets, code blocks, blank
|
||||||
|
structural_pattern = re.compile(
|
||||||
|
r"^\s*$|" # blank
|
||||||
|
r"^#+\s|" # heading
|
||||||
|
r"^```|" # code fence
|
||||||
|
r".*\[.*\]\(.*\)|" # contains a link
|
||||||
|
r"^\s*[-*]\s|" # bullet point
|
||||||
|
r"^@" # directive
|
||||||
|
)
|
||||||
|
in_code_block = False
|
||||||
|
structural = 0
|
||||||
|
for line in lines:
|
||||||
|
if line.strip().startswith("```"):
|
||||||
|
in_code_block = not in_code_block
|
||||||
|
structural += 1
|
||||||
|
elif in_code_block:
|
||||||
|
structural += 1 # code block contents are structural
|
||||||
|
elif structural_pattern.match(line):
|
||||||
|
structural += 1
|
||||||
|
prose = len(lines) - structural
|
||||||
|
|
||||||
|
# If more than 50% of AGENTS.md is prose, it's becoming a manual
|
||||||
|
if non_empty and prose / len(non_empty) > 0.50:
|
||||||
|
warn(
|
||||||
|
"DENSITY",
|
||||||
|
f"AGENTS.md is {prose / len(non_empty):.0%} prose — "
|
||||||
|
f"keep it as an index with pointers. Move prose to .agents/docs/.",
|
||||||
|
)
|
||||||
|
warnings += 1
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
ALL_CHECKS = [
|
||||||
|
("Line budgets", check_line_budget),
|
||||||
|
("Broken links", check_broken_links),
|
||||||
|
("Orphan docs", check_orphan_docs),
|
||||||
|
("Duplication", check_duplication),
|
||||||
|
("Stale references", check_stale_references),
|
||||||
|
("Index density", check_index_density),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
paths = _default_paths()
|
||||||
|
print(f"Agent context rot check: {paths['repo_root']}\n")
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for name, fn in ALL_CHECKS:
|
||||||
|
print(f"Checking {name}...")
|
||||||
|
count = fn(**paths)
|
||||||
|
total += count
|
||||||
|
if count == 0:
|
||||||
|
print(" OK")
|
||||||
|
|
||||||
|
print()
|
||||||
|
if total == 0:
|
||||||
|
print("All clean — agent context is lean.")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print(f"{total} warning(s) found — review above to keep context rot-free.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
0
.agents/tools/tests/__init__.py
Normal file
0
.agents/tools/tests/__init__.py
Normal file
309
.agents/tools/tests/test_clean_rot.py
Normal file
309
.agents/tools/tests/test_clean_rot.py
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
"""
|
||||||
|
Tests for .agents/tools/clean_rot.py
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
python -m pytest .agents/tools/tests/ -v
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Make the tools package importable
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from clean_rot import (
|
||||||
|
_extract_paragraphs,
|
||||||
|
check_broken_links,
|
||||||
|
check_duplication,
|
||||||
|
check_index_density,
|
||||||
|
check_line_budget,
|
||||||
|
check_orphan_docs,
|
||||||
|
check_stale_references,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo(tmp_path):
|
||||||
|
"""Scaffold a minimal fake repo for testing."""
|
||||||
|
agents_md = tmp_path / "AGENTS.md"
|
||||||
|
docs_dir = tmp_path / ".agents" / "docs"
|
||||||
|
src_dir = tmp_path / "evennia"
|
||||||
|
docs_dir.mkdir(parents=True)
|
||||||
|
src_dir.mkdir()
|
||||||
|
return {
|
||||||
|
"repo_root": tmp_path,
|
||||||
|
"agents_md": agents_md,
|
||||||
|
"docs_dir": docs_dir,
|
||||||
|
"src_dir": src_dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---- check_line_budget ----
|
||||||
|
|
||||||
|
|
||||||
|
class TestLineBudget:
|
||||||
|
def test_clean(self, repo):
|
||||||
|
repo["agents_md"].write_text("# Index\n\n- [Foo](foo.md)\n")
|
||||||
|
assert check_line_budget(**repo) == 0
|
||||||
|
|
||||||
|
def test_missing_agents_md(self, repo):
|
||||||
|
assert check_line_budget(**repo) == 1
|
||||||
|
|
||||||
|
def test_agents_md_over_budget(self, repo):
|
||||||
|
repo["agents_md"].write_text("\n".join(f"line {i}" for i in range(50)))
|
||||||
|
assert check_line_budget(**repo) == 1
|
||||||
|
|
||||||
|
def test_agents_md_at_budget(self, repo):
|
||||||
|
repo["agents_md"].write_text("\n".join(f"line {i}" for i in range(40)))
|
||||||
|
assert check_line_budget(**repo) == 0
|
||||||
|
|
||||||
|
def test_doc_over_budget(self, repo):
|
||||||
|
repo["agents_md"].write_text("# Index\n")
|
||||||
|
(repo["docs_dir"] / "big.md").write_text("\n".join(f"line {i}" for i in range(150)))
|
||||||
|
assert check_line_budget(**repo) == 1
|
||||||
|
|
||||||
|
def test_doc_under_budget(self, repo):
|
||||||
|
repo["agents_md"].write_text("# Index\n")
|
||||||
|
(repo["docs_dir"] / "small.md").write_text("# Small doc\n\nSome content.\n")
|
||||||
|
assert check_line_budget(**repo) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---- check_broken_links ----
|
||||||
|
|
||||||
|
|
||||||
|
class TestBrokenLinks:
|
||||||
|
def test_clean(self, repo):
|
||||||
|
(repo["docs_dir"] / "arch.md").write_text("# Architecture\n")
|
||||||
|
repo["agents_md"].write_text("See [Arch](.agents/docs/arch.md)\n")
|
||||||
|
assert check_broken_links(**repo) == 0
|
||||||
|
|
||||||
|
def test_broken_link_in_agents(self, repo):
|
||||||
|
repo["agents_md"].write_text("See [Missing](does/not/exist.md)\n")
|
||||||
|
assert check_broken_links(**repo) == 1
|
||||||
|
|
||||||
|
def test_http_links_skipped(self, repo):
|
||||||
|
repo["agents_md"].write_text("See [Docs](https://example.com)\n")
|
||||||
|
assert check_broken_links(**repo) == 0
|
||||||
|
|
||||||
|
def test_anchor_links_skipped(self, repo):
|
||||||
|
repo["agents_md"].write_text("See [Section](#section)\n")
|
||||||
|
assert check_broken_links(**repo) == 0
|
||||||
|
|
||||||
|
def test_broken_link_in_doc(self, repo):
|
||||||
|
repo["agents_md"].write_text("# Index\n")
|
||||||
|
(repo["docs_dir"] / "arch.md").write_text("See [Nope](nonexistent.md)\n")
|
||||||
|
assert check_broken_links(**repo) == 1
|
||||||
|
|
||||||
|
def test_valid_link_in_doc(self, repo):
|
||||||
|
repo["agents_md"].write_text("# Index\n")
|
||||||
|
(repo["docs_dir"] / "a.md").write_text("See [B](b.md)\n")
|
||||||
|
(repo["docs_dir"] / "b.md").write_text("# B\n")
|
||||||
|
assert check_broken_links(**repo) == 0
|
||||||
|
|
||||||
|
def test_no_agents_md(self, repo):
|
||||||
|
assert check_broken_links(**repo) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---- check_orphan_docs ----
|
||||||
|
|
||||||
|
|
||||||
|
class TestOrphanDocs:
|
||||||
|
def test_clean(self, repo):
|
||||||
|
(repo["docs_dir"] / "arch.md").write_text("# Arch\n")
|
||||||
|
repo["agents_md"].write_text("See [Arch](.agents/docs/arch.md)\n")
|
||||||
|
assert check_orphan_docs(**repo) == 0
|
||||||
|
|
||||||
|
def test_orphan_detected(self, repo):
|
||||||
|
(repo["docs_dir"] / "arch.md").write_text("# Arch\n")
|
||||||
|
(repo["docs_dir"] / "secret.md").write_text("# Hidden\n")
|
||||||
|
repo["agents_md"].write_text("See [Arch](.agents/docs/arch.md)\n")
|
||||||
|
assert check_orphan_docs(**repo) == 1
|
||||||
|
|
||||||
|
def test_no_docs_dir(self, repo):
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil.rmtree(repo["docs_dir"])
|
||||||
|
repo["agents_md"].write_text("# Index\n")
|
||||||
|
assert check_orphan_docs(**repo) == 0
|
||||||
|
|
||||||
|
def test_no_agents_md(self, repo):
|
||||||
|
(repo["docs_dir"] / "arch.md").write_text("# Arch\n")
|
||||||
|
assert check_orphan_docs(**repo) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---- _extract_paragraphs ----
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractParagraphs:
|
||||||
|
def test_skips_headings_and_bullets(self):
|
||||||
|
text = textwrap.dedent("""\
|
||||||
|
# Heading
|
||||||
|
|
||||||
|
- bullet point
|
||||||
|
|
||||||
|
This is a real paragraph that is long enough to pass the minimum length threshold for checking.
|
||||||
|
""")
|
||||||
|
paras = _extract_paragraphs(text)
|
||||||
|
assert len(paras) == 1
|
||||||
|
assert "real paragraph" in paras[0]
|
||||||
|
|
||||||
|
def test_skips_code_blocks(self):
|
||||||
|
text = textwrap.dedent("""\
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a real paragraph that is long enough to pass the minimum length threshold for checking.
|
||||||
|
""")
|
||||||
|
paras = _extract_paragraphs(text)
|
||||||
|
assert len(paras) == 1
|
||||||
|
assert "make test" not in paras[0]
|
||||||
|
|
||||||
|
def test_skips_short_paragraphs(self):
|
||||||
|
text = "Short.\n\nAlso short.\n"
|
||||||
|
assert _extract_paragraphs(text) == []
|
||||||
|
|
||||||
|
def test_joins_multiline_paragraph(self):
|
||||||
|
text = (
|
||||||
|
"This is the first line of a paragraph that will be joined together "
|
||||||
|
"with the second line.\n"
|
||||||
|
"This is the second line of that same paragraph which continues the thought.\n"
|
||||||
|
)
|
||||||
|
paras = _extract_paragraphs(text)
|
||||||
|
assert len(paras) == 1
|
||||||
|
assert "first line" in paras[0]
|
||||||
|
assert "second line" in paras[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ---- check_duplication ----
|
||||||
|
|
||||||
|
|
||||||
|
class TestDuplication:
|
||||||
|
def test_clean_no_overlap(self, repo):
|
||||||
|
repo["agents_md"].write_text(
|
||||||
|
"This is unique content in the index file that does not appear anywhere else "
|
||||||
|
"in the documentation tree at all.\n"
|
||||||
|
)
|
||||||
|
(repo["docs_dir"] / "arch.md").write_text(
|
||||||
|
"This is completely different content about architecture that shares nothing "
|
||||||
|
"with the index file whatsoever.\n"
|
||||||
|
)
|
||||||
|
assert check_duplication(**repo) == 0
|
||||||
|
|
||||||
|
def test_duplicate_detected(self, repo):
|
||||||
|
shared = (
|
||||||
|
"Tests use Django's test runner not pytest. The Makefile creates a test game dir "
|
||||||
|
"runs migrations then runs evennia test with keepdb.\n"
|
||||||
|
)
|
||||||
|
repo["agents_md"].write_text(shared)
|
||||||
|
(repo["docs_dir"] / "testing.md").write_text(shared)
|
||||||
|
assert check_duplication(**repo) >= 1
|
||||||
|
|
||||||
|
def test_no_docs_dir(self, repo):
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil.rmtree(repo["docs_dir"])
|
||||||
|
repo["agents_md"].write_text("# Index\n")
|
||||||
|
assert check_duplication(**repo) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---- check_stale_references ----
|
||||||
|
|
||||||
|
|
||||||
|
class TestStaleReferences:
|
||||||
|
def test_valid_slash_path(self, repo):
|
||||||
|
(repo["src_dir"] / "objects").mkdir()
|
||||||
|
repo["agents_md"].write_text("Models in `objects/models.py` etc.\n")
|
||||||
|
# create the file so it resolves
|
||||||
|
(repo["src_dir"] / "objects" / "models.py").write_text("")
|
||||||
|
assert check_stale_references(**repo) == 0
|
||||||
|
|
||||||
|
def test_stale_slash_path(self, repo):
|
||||||
|
repo["agents_md"].write_text("See `objects/nonexistent/` for details.\n")
|
||||||
|
assert check_stale_references(**repo) == 1
|
||||||
|
|
||||||
|
def test_valid_dotted_module(self, repo):
|
||||||
|
mod_dir = repo["repo_root"] / "evennia" / "objects"
|
||||||
|
mod_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(mod_dir / "tests.py").write_text("")
|
||||||
|
repo["agents_md"].write_text("Run `evennia.objects.tests` for tests.\n")
|
||||||
|
assert check_stale_references(**repo) == 0
|
||||||
|
|
||||||
|
def test_stale_dotted_module(self, repo):
|
||||||
|
repo["agents_md"].write_text("Run `evennia.nonexistent.module` for details.\n")
|
||||||
|
assert check_stale_references(**repo) == 1
|
||||||
|
|
||||||
|
def test_bare_filenames_ignored(self, repo):
|
||||||
|
repo["agents_md"].write_text("Edit `cmdhandler.py` and use `uv` to install.\n")
|
||||||
|
assert check_stale_references(**repo) == 0
|
||||||
|
|
||||||
|
def test_contrib_path_resolved(self, repo):
|
||||||
|
contrib = repo["src_dir"] / "contrib" / "base_systems"
|
||||||
|
contrib.mkdir(parents=True)
|
||||||
|
repo["agents_md"].write_text("See `base_systems/` in contrib.\n")
|
||||||
|
assert check_stale_references(**repo) == 0
|
||||||
|
|
||||||
|
def test_game_template_path_resolved(self, repo):
|
||||||
|
tmpl = repo["src_dir"] / "game_template" / "server" / "conf"
|
||||||
|
tmpl.mkdir(parents=True)
|
||||||
|
(tmpl / "settings.py").write_text("")
|
||||||
|
repo["agents_md"].write_text("Games override in `server/conf/settings.py`.\n")
|
||||||
|
assert check_stale_references(**repo) == 0
|
||||||
|
|
||||||
|
def test_no_agents_md(self, repo):
|
||||||
|
assert check_stale_references(**repo) == 0
|
||||||
|
|
||||||
|
def test_checks_docs_dir_files_too(self, repo):
|
||||||
|
repo["agents_md"].write_text("# Index\n")
|
||||||
|
(repo["docs_dir"] / "arch.md").write_text("See `gone/deleted/` for info.\n")
|
||||||
|
assert check_stale_references(**repo) == 1
|
||||||
|
|
||||||
|
|
||||||
|
# ---- check_index_density ----
|
||||||
|
|
||||||
|
|
||||||
|
class TestIndexDensity:
|
||||||
|
def test_mostly_structural(self, repo):
|
||||||
|
repo["agents_md"].write_text(textwrap.dedent("""\
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Section
|
||||||
|
|
||||||
|
- [Link](foo.md)
|
||||||
|
- [Link](bar.md)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
"""))
|
||||||
|
assert check_index_density(**repo) == 0
|
||||||
|
|
||||||
|
def test_too_much_prose(self, repo):
|
||||||
|
# All non-structural lines
|
||||||
|
prose_lines = [f"This is prose line number {i}." for i in range(30)]
|
||||||
|
repo["agents_md"].write_text("\n".join(prose_lines) + "\n")
|
||||||
|
assert check_index_density(**repo) == 1
|
||||||
|
|
||||||
|
def test_no_agents_md(self, repo):
|
||||||
|
assert check_index_density(**repo) == 0
|
||||||
|
|
||||||
|
def test_empty_file(self, repo):
|
||||||
|
repo["agents_md"].write_text("")
|
||||||
|
assert check_index_density(**repo) == 0
|
||||||
|
|
||||||
|
def test_code_block_contents_are_structural(self, repo):
|
||||||
|
repo["agents_md"].write_text(textwrap.dedent("""\
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
make format
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
One prose line here.
|
||||||
|
"""))
|
||||||
|
assert check_index_density(**repo) == 0
|
||||||
35
AGENTS.md
Normal file
35
AGENTS.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to AI coding agents working in this repository.
|
||||||
|
|
||||||
|
Evennia is a Python (>= 3.12) framework for building text-based multiplayer online games (MUD/MUX/MUSH/MOO). It is a library, not a game.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
Use `uv run` to execute commands in the project venv. Prefer `uv` over `pip`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # full test suite (Evennia/Django)
|
||||||
|
make tests=evennia.objects.tests test # specific module
|
||||||
|
make format # black + isort
|
||||||
|
make lint # black --check
|
||||||
|
make cleanrot # check agent context for rot
|
||||||
|
uv pip install -e . # dev install
|
||||||
|
uv run pytest .agents/tools/tests/ -v # agent tooling tests (pytest)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
- **TDD**: write tests first. Prefer no-DB tests > mocks > full DB-backed tests.
|
||||||
|
- **Don't manually format code.** Run `make format` after editing.
|
||||||
|
- **All code must have Google-style docstrings.** See [Code Style](.agents/docs/code-style.md).
|
||||||
|
- After editing agent context files, run `make cleanrot`.
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [Core Beliefs](.agents/docs/core-beliefs.md) — design principles that guide tradeoffs (toolkit not game, think in Python not SQL, hooks not patches, compose don't branch)
|
||||||
|
- [Architecture](.agents/docs/architecture.md) — two-process model, typeclass system, command system, subsystems, flat API, settings
|
||||||
|
- [Testing](.agents/docs/testing.md) — running tests, test base classes, DB setup, CI matrix
|
||||||
|
- [Code Style](.agents/docs/code-style.md) — docstring conventions, command docstring format
|
||||||
|
- [Development Commands](.agents/docs/commands.md) — install, game lifecycle, test/format commands, PR conventions
|
||||||
|
- [CI/CD](.agents/docs/ci.md) — GitHub Actions workflows, test matrix, database configs, Docker, secrets
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Main branch
|
## Main branch
|
||||||
|
|
||||||
|
Feat: Add AGENTS.md and .agents context files to aid AI agent development (Griatch)
|
||||||
|
Feat: Add `uv.lock` for Evennia library developers wanting to use the `uv` tool (Griatch)
|
||||||
Fix: Improve indentation/formatting for east-asian languages (Griatch, with inspiration from hhsiao)
|
Fix: Improve indentation/formatting for east-asian languages (Griatch, with inspiration from hhsiao)
|
||||||
Docs: Griatch
|
Docs: Griatch
|
||||||
|
|
||||||
|
|||||||
6
Makefile
6
Makefile
@ -13,7 +13,8 @@ default:
|
|||||||
@echo " make test - run evennia test suite with all default values."
|
@echo " make test - run evennia test suite with all default values."
|
||||||
@echo " make tests=evennia.path test - run only specific test or tests."
|
@echo " make tests=evennia.path test - run only specific test or tests."
|
||||||
@echo " make testp - run test suite using multiple cores."
|
@echo " make testp - run test suite using multiple cores."
|
||||||
@echo " make release - publish evennia to pypi (requires pypi credentials)
|
@echo " make release - publish evennia to pypi (requires pypi credentials)"
|
||||||
|
@echo " make cleanrot - check agent context docs for rot/bloat"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
pip install -e .
|
pip install -e .
|
||||||
@ -49,3 +50,6 @@ version:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
./.release.sh
|
./.release.sh
|
||||||
|
|
||||||
|
cleanrot:
|
||||||
|
python .agents/tools/clean_rot.py
|
||||||
|
|||||||
Reference in New Issue
Block a user