Files
jpmschweitzerandClaude b4d5c4d9b2 docs: correct the vault policy, and qualify workspace decision ids
This repo's Work tracking section still described the reversed policy --
that tickets and decisions live in the workspace vault and this repo's
trees stay empty. That was overturned the same day: repo vaults are
standalone and a repo's work travels with a clone, because the changelog
is committed.

Every other repo was corrected at the time; this one was missed because
the search for the offending phrase used a fixed string and the phrase
happened to wrap across a line break here. Worth noting as a search
failure rather than a writing one -- five files were checked, four
matched, and the fifth was reported clean.

Decision ids are also qualified now. They are per-vault sequences, so a
bare D-15 here will mean this repo's D-15 the moment this repo records
one; pql already holds D-1 through D-31 against the workspace's D-1
through D-21, all of them unrelated.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-09 04:17:29 +02:00

10 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. 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.

/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:

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