Files
webber/CLAUDE.md
T
jpmschweitzerandClaude 8eec3b68d6 docs: replace both AGENTS.md files with one CLAUDE.md
One agent doc per repo, and it is CLAUDE.md. This repo carried two --
one at the root and one under webber-api/ -- which is the drift problem
in its purest form: two documents, one subject, and no way to know which
the last reader trusted. Written fresh rather than reformatted.

README.md linked to webber-api/AGENTS.md, so that pointer moves with the
file rather than dangling.

The architecture section states the method used to establish what is
live -- import the app inside the container and read sys.modules -- and
then the case where that method fails here. src/domains/tools is absent
from a cold snapshot and is entirely live: each agent's _register_tools
imports its tool package from inside the method body, on every
/agents/run. Absence from a snapshot taken before any request is served
is a timing artifact, not evidence of death, and deleting on that basis
would have removed the tool layer.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-09 03:14:50 +02:00

9.9 KiB

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/<name>/. 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/<name>/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.

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. Tickets and decisions for this repo live in the workspace vault; this repo's .pql/ and governance/ trees stay empty (D-15).

Two things must be spelled out on every invocation from in here, and each fails differently:

  • pql is not on the non-interactive PATH — use /home/jpmschweitzer/.local/bin/pql.
  • --vault /mnt/media/Projects is mandatory. pql anchors a vault at the nearest .git/ ancestor, and this repo is one, so a bare call resolves to this repo's empty vault. Reads return nothing; a write creates a stray vault and starts ticket ids at T-1, colliding with the real ones.
/home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects ticket list
/home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects plan whatsnext
/home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects decisions list --domain webber

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.