Files
webber/CLAUDE.md
T
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

190 lines
10 KiB
Markdown

# 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.
```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.