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>
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/taskagent packages),src.domains.conversations,src.shared.*,src.ollama,src.db(imported both byconversations/router.pyat module scope and bymain.py's lifespan shutdown handler). - Present in source, explicitly disabled:
src/domains/router.pyhas# from src.domains.auth.router import router as auth_routerand the equivalent fortools_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 insys.modulesright afterimport src.main, but it is not dead.src/domains/agents/{explore,plan,task}/agent.pyeach have a method (e.g.PlanAgent._register_tools) that doesfrom src.domains.agents.plan.tools import register_plan_toolsinside the function body, called every time that agent is constructed — i.e. on every/agents/runor/agents/streamrequest for that agent type. That nested module then imports the real tool classes fromsrc.domains.tools.file,.search,.shellat module scope. A static snapshot taken before any request is served will not showsrc.domains.toolsloaded; that is a timing artifact, not evidence it is unused. Don't deletesrc/domains/tools/on the strength of asys.modulescheck alone — confirm by hitting/agents/runand re-checking, or by tracing the call graph from each agent's_register_tools. src/cli/is the implementation behindwebber-cli'spyproject.tomlscript 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'sAGENTS.mdpreviously mandatedfeature/...orfix/...branches for every change and forbade committing tomaindirectly — 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 ownCHANGELOG.md(webber-api/CHANGELOG.md,webber-cli/CHANGELOG.md); the rootCHANGELOG.mdis just an index pointing at both.
Releasing (API)
Ask whether a deploy is wanted first — it is not automatic.
- Bump the version in
webber-api/pyproject.toml. - Move
[Unreleased]entries into a dated version section inwebber-api/CHANGELOG.md. - Stage the changed files by name, commit, tag
api/vX.Y.Z,git push origin main --tags. - Gitea CI (
build-api.yml) builds and pushes the image on the tag; Watchtower deploys it. - 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: nullworkaround:src/ollama/provider.py(confirmed present, loaded at startup per thesys.modulescheck above) sanitizescontent: nulltocontent: ""for assistant messages with tool calls, working around an Ollama API limitation.