Tooling Stack/Chapter 15 of 35

Repo conventions

5 min readEdit on GitHub

A repo layout that is friendly to humans and agents. Both audiences benefit from the same property: predictability.

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 (or justfile) 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

PatternWhy
train_<task>.py, eval_<task>.pyPredictable. 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>.pyPytest auto-discovery + readable.
Snake case for files, dataclasses for configsPythonic, 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.