# CLAUDE.md — webber Local-LLM multi-agent development assistant — "similar to Claude Code but running locally" (`webber-api/docs/architecture.md`), backed by Ollama via PydanticAI. Logical monorepo, single `.git`, three subprojects: `webber-api/` (FastAPI server, deployed), `webber-cli/` (Typer CLI client), `webber-sandbox/` (swappable test project used by `sandbox.sh`, not shipped). ## Ports | | Port | How | |---|---|---| | Local dev | **8095** | `cd webber-api && ./wakeup.sh`, uvicorn `--reload`, logs to `webber-api/logs/server.log` | | Production | **8086** | container `webber`, confirmed running (`docker ps`) on `docker-dataplane` | `wakeup.sh` refuses to start if 8095 is already bound — it does not silently pick another port. Testing `localhost:8086` on the dev box hits the *container*, not your reload server. ## Live contract `http://localhost:8086/openapi.json` — 10 paths, `version: 1.0.1` (verified 2026-08-09, matches `webber-api/pyproject.toml` and the live `/health` response). Human docs at `http://localhost:8086/docs`. Query the live spec rather than inferring routes from source — `src/domains/router.py` currently has two routers commented out (see Architecture), so a source read alone will overcount if you don't check whether an include is live. ``` /, /health, /agents/, /agents/run, /agents/stream, /agents/{agent_type}, /conversations/, /conversations/{conversation_id}, /conversations/{conversation_id}/messages, /conversations/{conversation_id}/save ``` ## Architecture Domain-first layout under `webber-api/src/domains//`. `src/main.py` includes exactly one router, `src.domains.router.root_router`, which composes the domain routers. A full directory map lives in `webber-api/docs/architecture.md` — read that before adding a domain rather than duplicating it here. **How liveness below was established:** `docker exec webber python3 -c "import src.main; import sys; print(sorted(m for m in sys.modules if m.startswith('src.')))"` — i.e. importing the real app inside the running container and reading `sys.modules`, not grepping `main.py`. Re-run that command to re-check; a grep of imports will miss function-body imports, and this repo has one that matters. - **Wired at startup, serving routes:** `src.domains.health`, `src.domains.agents` (router + `explore`/`plan`/`task` agent packages), `src.domains.conversations`, `src.shared.*`, `src.ollama`, `src.db` (imported both by `conversations/router.py` at module scope and by `main.py`'s lifespan shutdown handler). - **Present in source, explicitly disabled:** `src/domains/router.py` has `# from src.domains.auth.router import router as auth_router` and the equivalent for `tools_router` — both commented out with the include calls also commented out. `src/domains/auth/` is just an empty `__init__.py`. This one *is* dead — the disabling is visible in the same file, not a matter of tracing an indirect import. - **The trap: `src/domains/tools/` is not in `sys.modules` right after `import src.main`, but it is not dead.** `src/domains/agents/{explore,plan,task}/agent.py` each have a method (e.g. `PlanAgent._register_tools`) that does `from src.domains.agents.plan.tools import register_plan_tools` **inside the function body**, called every time that agent is constructed — i.e. on every `/agents/run` or `/agents/stream` request for that agent type. That nested module then imports the real tool classes from `src.domains.tools.file`, `.search`, `.shell` at module scope. A static snapshot taken before any request is served will not show `src.domains.tools` loaded; that is a timing artifact, not evidence it is unused. Don't delete `src/domains/tools/` on the strength of a `sys.modules` check alone — confirm by hitting `/agents/run` and re-checking, or by tracing the call graph from each agent's `_register_tools`. - **`src/cli/`** is the implementation behind `webber-cli`'s `pyproject.toml` script entry — it is a separate Typer app, not imported by the API (`src.main`) at all. Its liveness is "is the CLI installed and invoked", not "is it wired into the API process". Group new work by domain, not file type — `webber-api/docs/fastapi-best-practices.md` is the house reference (mirrors the convention used across the other in-house FastAPI services here). ## Database SQLite by default (`database_url = "sqlite+aiosqlite:///./webber.db"` in `src/shared/config.py`), not Postgres — confirmed by reading `src/shared/config.py` and `src/db/database.py` (the latter's docstring says the pattern is ported from core-api, but the backend differs). Models under `webber-api/src/domains//models.py` import `Base` from `src/db/models.py`. No Alembic here (unlike core-api) — did not find a migrations directory; unverified whether schema changes have any managed migration path at all. Check before assuming one exists. ## Working here **Test locally first.** `cd webber-api && ./wakeup.sh` auto-reloads on code changes (not on `requirements.txt` changes — restart after adding a dependency). Deploy only once a feature is complete and tested. ```bash cd webber-api .venv/bin/python -m pytest tests/ # all tests .venv/bin/python -m pytest tests/ -v --cov # verbose + coverage .venv/bin/python -m pytest tests/test_tools.py -v # single file ``` `webber-api/pyproject.toml` declares `[tool.ruff]` and `[tool.mypy]` — unlike core-api, this repo does have ruff/mypy config; whether either runs in CI is a separate question (see CI below — it does not). Copy `webber-api/.env.example` to `webber-api/.env`. Notable defaults: `OLLAMA_URL` points at `192.168.86.149:11434` (the host's Ollama, not a container), `OLLAMA_AGENT_MODEL=gemma4:e2b`, optional Tatlock integration via `TATLOCK_API_URL`/`INTERNAL_API_KEY`, optional SearXNG via `SEARXNG_URL` for the `web_search` tool. ### Sandbox `webber-sandbox/` is a disposable project used to exercise the agents end-to-end, managed by `./sandbox.sh {list,load,reset,save,status}` from the repo root. `sandbox-templates/` holds the reusable templates (`calculator-cli` has intentionally-seeded bugs for testing Explore/Task). This directory is fixture material, not shipped code — do not treat bugs in it as real bugs. ### CLI `webber-cli/` is a Typer client (`webber-cli status|chat|explore|sessions|config`) with tab completion, session persistence (`~/.webber_history`, `~/.webber/config.toml`), and three chat modes (`plan` read-only, `default`, `auto_accept`). It talks to the API over HTTP — it does not share a process with `webber-api`. Run it from its own venv: `cd webber-cli && .venv/bin/webber-cli status`. ## CI `.gitea/workflows/build-api.yml` triggers only on `api/vX.Y.Z` tags: creates a Gitea release, builds/pushes `git.schweitz.net/jpmschweitzer/webber-api`, then pings Watchtower. `build-cli.yml` triggers on `cli/vX.Y.Z` tags but is a placeholder — it only echoes a TODO, it does not build or publish anything. **No test or lint gate runs in CI for either package** — pytest and ruff only run locally or on request. Verify tests pass before tagging. ## Work tracking Work lives in **pql**, not a markdown TODO or `docs/COVERAGE.md`. **This repo's vault is standalone** — its tickets and its internal decisions live here in `.pql/` and `governance/`, and travel with a clone, because `.pql/changelog/` is committed and replayed by the git hooks (workspace D-15). The databases are gitignored and rebuildable with `pql plan rebuild`. `pql` is **not** on the non-interactive `PATH` — invoke it as `/home/jpmschweitzer/.local/bin/pql`. From inside this repo no `--vault` is needed: pql anchors at the nearest `.git/` ancestor, which is this repo. ```bash /home/jpmschweitzer/.local/bin/pql ticket list # this repo's open work /home/jpmschweitzer/.local/bin/pql plan whatsnext # next unblocked item, with context /home/jpmschweitzer/.local/bin/pql decisions list # this repo's own decisions ``` Stack-level decisions that constrain this service live in the **workspace** vault and need the flag: ```bash /home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects decisions list --domain webber ``` Note `ticket new --decision D-N` resolves ids within **one** vault, so a ticket here cannot link to a workspace decision. Cite the id in the ticket body instead. Do not add a TODO section to a markdown file. ## Git - **History is linear — no merge commits.** Work on `main`, or a short-lived branch that is fast-forwarded and deleted. (This repo's `AGENTS.md` previously mandated `feature/...` or `fix/...` branches for every change and forbade committing to `main` directly — that rule was retired workspace-wide on 2026-08-08 and does not apply here anymore.) - **Conventional Commits**: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:`. - **Atomic commits** — one logical change each. - **Stage explicitly. Never `git add -A`** — denied by policy; it sweeps in whatever else is dirty, including secrets. - Each package versions independently via prefixed tags (`api/vX.Y.Z`, `cli/vX.Y.Z`) and its own `CHANGELOG.md` (`webber-api/CHANGELOG.md`, `webber-cli/CHANGELOG.md`); the root `CHANGELOG.md` is just an index pointing at both. ## Releasing (API) Ask whether a deploy is wanted first — it is not automatic. 1. Bump the version in `webber-api/pyproject.toml`. 2. Move `[Unreleased]` entries into a dated version section in `webber-api/CHANGELOG.md`. 3. Stage the changed files by name, commit, tag `api/vX.Y.Z`, `git push origin main --tags`. 4. Gitea CI (`build-api.yml`) builds and pushes the image on the tag; Watchtower deploys it. 5. Verify: `curl http://192.168.86.149:8086/health`. CLI releases (`cli/vX.Y.Z`) currently only log a TODO in CI — there is no build/publish step to trigger yet. ## Known issues (carried over, unverified beyond what's stated) - **Model hallucination**: the Explore agent's model can hallucinate file contents instead of using actual tool results, per `webber-api/AGENTS.md` — a mitigation (stronger model or response validation) was suggested there but not confirmed implemented. - **Ollama `content: null` workaround**: `src/ollama/provider.py` (confirmed present, loaded at startup per the `sys.modules` check above) sanitizes `content: null` to `content: ""` for assistant messages with tool calls, working around an Ollama API limitation.