Three migrations left their documentation behind: wakeup.sh was replaced by the Makefile during the project structure consolidation, but AGENTS.md and the e2e README still tell you to run it. The log path moved to build/logs/server.log at the same time. The local model moved to gemma4:e2b, but the e2e prerequisites and the benchmark recommendation still name mistral-nemo. The benchmark figures in CLAUDE.md predate the current model. Measured 2026-08-07: ~95 tok/s, full flow ~10-13s for simple turns, cold model load ~36s rather than ~8s. A turn costs three sequential Ollama calls and ~710 generated tokens regardless of how trivial the question is. Co-Authored-By: Claude <noreply@anthropic.com>
3.5 KiB
CLAUDE.md
Claude Code-specific notes for this project. For general development instructions, architecture, coding standards, and deployment — see AGENTS.md.
Setup & Commands
make setup # Create venv and install all dependencies
make test # Unit tests (no external services)
make test-integration # Integration tests (needs Claude/Ollama)
make test-contracts # Wire-level contract tests against live service boundaries
make run # Start dev server on port 8777
make lint # Ruff linter + formatter check
make typecheck # Mypy
make clean # Remove caches and build artifacts
Dependencies are in pyproject.toml ([project.dependencies] and [project.optional-dependencies.dev]).
Critical Gotchas
ASGITransport does NOT trigger FastAPI lifespan events. The session-scoped _initialize_app fixture in tests/conftest.py calls initialize_application() explicitly via asyncio.run(). Without this, the Ollama/Claude health checks never run: _ollama_available stays None (treated as available, so requests go to Ollama) and _claude_available stays None (treated as unavailable, so the Claude fallback never engages).
AsyncIO scope mismatch. asyncio_default_fixture_loop_scope = function is set in pyproject.toml. Session-scoped async fixtures cause ScopeMismatch errors. The fix is to use a sync fixture with asyncio.run() for session-scoped initialization.
The butler persona prompt suppresses local-model tool calling. With TATLOCK_SYSTEM_PROMPT attached, gemma4 reasons about calling the calculator, then answers from memory with wrong arithmetic (a different wrong product each run). orchestrate_tool_calls() therefore uses the terse TATLOCK_ORCHESTRATION_PROMPT; the persona is applied in synthesize_from_results(). Do not reattach the persona prompt to a tool-phase agent. tool_choice: "required" via extra_body does NOT force Ollama to call tools — it is advisory at best.
Claude Sonnet 5+ rejects sampling parameters. temperature/top_p/top_k return a 400. Use get_sampling_settings() from the model selector instead of passing ModelSettings(temperature=...) directly to agents that can run on the Claude fallback. The contract test suite pins this (make test-contracts).
Integration test timeouts. Set to 120s to match OLLAMA_TIMEOUT config (300s for the pure-Ollama fallback test, which cannot be rescued by Claude). Current GPU-resident numbers (measured 2026-08-07, gemma4:e2b at ~95 tok/s): full Steward → orchestrate → synthesize flow ~10–13s for simple turns; librarian-routed queries ~20-25s (not re-measured). A single turn costs 3 sequential Ollama calls and ~710 generated tokens even for "what is 61 plus 12?" — most of it the model's own reasoning, paid three times. Cold model load is ~36s, avoided while the model is pinned with keep_alive: -1; the OLLAMA_KEEP_ALIVE=2h default otherwise reintroduces it. The old "~35s steward / ~2 min flow" and "11–25s flow" figures are superseded — do not plan against them. STEWARD_TIMEOUT defaults to 60s.
get_benchmark_store does not exist. The benchmarking module (src/core/benchmarks.py) was never implemented. scripts/benchmark_analysis.py also references it and is broken. Do not add mocks for it in tests.
Steward tests need household registry. Use register_household_members() (sync) in fixtures, not initialize_application() (async). The steward extracts capabilities from the registry.