Repo conventions
A repo layout that is friendly to humans and agents. Both audiences benefit from the same property: predictability.
Recommended layout
my-ml-project/
├── CLAUDE.md # Hard rules for the agent
├── AGENTS.md # (optional) Tool-agnostic version
├── README.md # First page for humans
├── pyproject.toml # Single source for deps + tooling config
├── requirements.lock # Pinned deps (generated)
├──.python-version # Pin Python minor
├──.pre-commit-config.yaml # Format, lint, type-check on commit
├──.claude/
│ ├── commands/ # Slash commands
│ └── agents/ # Subagents
├── src/
│ └── my_pkg/
│ ├── __init__.py
│ ├── data.py # Loading, manifests, splits
│ ├── model.py # Model definitions
│ ├── train.py # Single training entry point
│ ├── eval.py # Single eval entry point
│ └── config.py # Config schema
├── tests/
│ ├── unit/ # < 30 sec, no GPU, no network
│ └── slow/ # Integration, GPU, full data
├── benchmarks/
│ ├── run.py # Top-level bench entry point
│ └── results/ # Committed JSON results
├── config/
│ ├── data.yaml # Dataset paths/manifests
│ └── train/
│ ├── default.yaml
│ └── ablation_*.yaml
├── data/
│ ├── manifests/ # Committed; tiny JSON pointing at storage
│ └── raw/ # Gitignored; populated by `make data`
├── runs/ # Gitignored; one dir per training run
├── docs/
│ ├── decisions/ # ADRs
│ ├── models/ # Model cards
│ └── BENCHMARK.md # Numbers + script references
├── scripts/ # Ad-hoc, throwaway is fine
├── Makefile # The discoverable interface
└──.env.example # Template;.env is gitignored
You do not need all of this. You do need:
- A single training entry point (no
train_v2.py,train_final.py). - A single eval entry point.
- A
runs/dir gitignored, populated by training. - A
Makefile(orjustfile) with the canonical commands.
The Makefile is the contract
A discoverable list of every operation that matters:
.PHONY: setup data train eval bench test fmt lint reproduce
setup:
uv venv && uv pip sync requirements.lock
data:
python scripts/fetch_data.py
train:
python -m my_pkg.train --config config/train/default.yaml
eval:
python -m my_pkg.eval --run-dir runs/$(RUN)
bench:
python benchmarks/run.py --run-dir runs/$(RUN) --out benchmarks/results/$(RUN).json
test:
pytest tests/unit -q
fmt:
ruff format src tests
lint:
ruff check src tests && mypy --strict src
reproduce:
@test -n "$(RUN)" || (echo "RUN=<run-id> required"; exit 1)
bash runs/$(RUN)/command.sh
Now anyone (human or agent) reads the Makefile and knows the whole interface
in 30 seconds. The agent does not have to guess at "how do I run tests in this
repo?" — the answer is make test.
Naming conventions worth fighting for
| Pattern | Why |
|---|---|
train_<task>.py, eval_<task>.py | Predictable. Agent can find the right entry point. |
runs/<YYYY-MM-DD>-<slug>/ | Sortable. Greppable. No "final_v2_REAL". |
metrics.json (always this name) | Tools and agents look here first. |
config.yaml (always this name) | Same. |
tests/unit/test_<thing>.py | Pytest auto-discovery + readable. |
| Snake case for files, dataclasses for configs | Pythonic, agent-predictable. |
The flip side: forbid noise. final/, temp/, old/, _backup/, v2/
directories. The agent will compose paths into wrong combinations. Delete or
move to a separate archive.
What goes in pyproject.toml
Centralize everything:
[project]
name = "my-pkg"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["torch", "transformers", "datasets", "pydantic"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "N", "RUF"]
[tool.mypy]
strict = true
files = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests/unit"]
addopts = "-q --strict-markers"
markers = ["slow: integration tests"]
One file, one source of truth. The agent reads this and knows your style.
Pre-commit: the cheap insurance
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
files: ^src/
args: [--strict]
The agent will sometimes write code that fails type-checking or linting.
Pre-commit catches it before it lands. Never let the agent bypass with
--no-verify.
What to gitignore
# python
__pycache__/
*.pyc
.venv/
.mypy_cache/
.ruff_cache/
# data and weights
data/raw/
runs/
*.pt
*.bin
*.parquet
# secrets
.env
*.key
If the agent ever stages something here, your CLAUDE.md should explicitly
forbid it. "Never git add files matching runs/, data/raw/, .env, or
weight extensions."
Agent-friendly is human-friendly
Notice that nothing above is agent-specific. A predictable layout, a discoverable Makefile, a single training entry point — these help any new contributor too. The agent benefits because it is treated like a new contributor with broad knowledge but no project-specific memory.
That is the right framing for everything in this section: design for new contributors and the agent will follow.