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>
190 lines
10 KiB
Markdown
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.
|