213 lines
12 KiB
Markdown
213 lines
12 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for Claude Code (claude.ai/code) working in this repository.
|
|
|
|
**This is a rules file.** It loads into every session, so it carries only what
|
|
changes what an agent does. A measurement belongs in `docs/evals/`, dated and
|
|
never edited after the day. A subsystem's reasoning belongs in its living doc
|
|
under `docs/`. Read that doc before changing the subsystem.
|
|
|
|
| Read this | Before |
|
|
|---|---|
|
|
| `docs/routing.md` | touching `internal/router/` or `queryWalk` |
|
|
| `docs/deployment.md` | touching a daemon, compose, a systemd unit or the web UI |
|
|
| `docs/offload.md` | touching a daemon seam or adding a model caller |
|
|
| `docs/world.md` | touching search, Kiwix or the world chain |
|
|
| `docs/language.md` | changing a prompt contract or a Russian word list |
|
|
| `docs/ecosystem.md` | touching Nexus, Praxis or Hexis |
|
|
| `docs/rearchitecture.md`, `docs/design.md` | changing the shape of anything |
|
|
| `docs/workflow.md` | the five stores, the doc tiers, the guards |
|
|
| `docs/caveats/` | a known limit, its task id and its revisit trigger |
|
|
| `docs/CLAUDE.md` | which tier a doc belongs in, and what each one holds |
|
|
| `AGENTS.md` | local preview, screenshots, model downloads |
|
|
|
|
## What Maven is
|
|
|
|
A self-hosted, privacy-first voice assistant in Russian and English. Go daemons
|
|
talk over unix sockets. One resident small model routes and phrases. whisper.cpp
|
|
does speech-to-text and piper does text-to-speech.
|
|
|
|
The resident model is **Qwen3-1.7B** (`UD-Q4_K_XL`) on homesrv, a Thinking
|
|
variant at `n_ctx` 4096. Keep it at 1.7B or under. Sub-500M models are unusable
|
|
in Russian (`docs/evals/2026-07-31-model-bakeoff.md`). Model files live in
|
|
`/mnt/hdd1/llms`, bind-mounted over the repo's `models/llm/`, so a gguf sitting
|
|
in the repo is loaded by nothing.
|
|
|
|
The workstation is workpc and it holds the remote model and speech-to-text.
|
|
**It is never assumed up.** **Fall back silently** when it would only do the job
|
|
better. **Name the gap** when the resident model cannot do the job at all.
|
|
|
|
**The embedder stays on homesrv permanently**, because it backs that floor.
|
|
`EmbedQuery` and `EmbedPassage` apply the `query:` and `passage:` prefixes
|
|
multilingual-e5-small was trained with. Calling plain `Embed` on a note is a bug.
|
|
|
|
## Build and test
|
|
|
|
CGO daemons (`mavend`, `mavsttd`, `mavttsd`, `mavenclient`) need the vendored
|
|
toolchain wired through the Makefile. **Do not call `go build` on them bare**,
|
|
and **do not hand-write the CGO preamble**. This box runs zsh, so an unquoted
|
|
`-run Test*` dies on "no matches found" before `go` is reached. `make t` also
|
|
carries `-count=1` and sets `MAVEN_ONNX_LIB`. Without that variable the four
|
|
`TestONNX*` measurements self-skip and the run still prints `ok`.
|
|
|
|
```sh
|
|
make build # all 11 binaries. make build-web for one (web/waked/poll/caldav skip CGO)
|
|
make test # go test -race across ./internal/... ./cmd/... with CGO env set
|
|
make t PKG=./internal/router/eval/ RUN='TestONNX' V=1 # V=1 for -v, RACE=0 to drop -race
|
|
make analyze # staticcheck, deadcode and govulncheck. Not in `test`: all three need the network
|
|
```
|
|
|
|
**The static gates pass against a baseline, not against zero**
|
|
(`scripts/analyzers/*.baseline`, reasoning in `docs/workflow.md`). A fix must
|
|
delete its baseline entry, because the gate also fails on an entry whose finding
|
|
is gone. **`make audit` is a git-grep inventory, not analysis.** Do not cite it
|
|
as a reachability check.
|
|
|
|
## The daemons
|
|
|
|
Eleven binaries under `cmd/`, wired socket-to-socket over `internal/ipc`, not
|
|
linked. `mavend` is the core and owns the DB and the IPC socket.
|
|
`deploy/mavend.json` sets sockets, model paths and the phraser and embedder
|
|
blocks, with `${VAR}` expansion from gitignored `deploy/telegram.env`.
|
|
**`docker-compose.yml` runs five**: `mavend`, `mavsttd`, `mavttsd`, `mavweb`,
|
|
`mavpoll`. Count against compose, not against `make build`. `mavwaked` runs on
|
|
workpc under systemd. `docs/deployment.md` says who else is absent and why.
|
|
|
|
- **Passwords are read from files, never taken as flag values.**
|
|
- **The voice wire is plaintext with no auth.** mavend's voice port stays on
|
|
homesrv loopback and reaches workpc over ssh. Do not LAN-bind it.
|
|
`SurfaceVoice` caps acts at L0, and L0 does not cap reading.
|
|
- **A GPU service added beside mavgpud goes in `cmd/mavgpud`, never in systemd.**
|
|
The card needs one owner. A second unit made mavgpud evict llama-server every
|
|
few seconds and took the model arm down for eight minutes.
|
|
|
|
## The ecosystem: Nexus, Praxis, Hexis
|
|
|
|
Nexus identifies, Praxis observes, Hexis acts, Maven understands. Maven does not
|
|
own identity, operational state, or execution. Full contract in
|
|
`docs/ecosystem.md`. All three are `nil` unless configured and each degrades
|
|
alone. An outage means a named gap, never a broken turn or a guess.
|
|
|
|
- **No component reads another component's database.** Praxis attention comes
|
|
over HTTP, never from its SQLite file.
|
|
- **Identity lives in Nexus.** Do not invent a local fact key for something
|
|
Nexus resolves. `cmd/mavend/factenrichment.go` resolves `actionFact.Subject`.
|
|
- **Free text never reaches a mutating Hexis call.** Resolve to a canonical
|
|
entity id first. Ambiguous resolution asks the owner, it does not pick.
|
|
- **LLM output is not authorization.** Confirmation binds capability id, target
|
|
entity, arguments, requester and expiry (`cmd/mavend/confirm.go`).
|
|
- **Praxis lifecycle words differ.** Surfaced is not acknowledged, acknowledged
|
|
is not resolved, execution success is not recovery. Reading an item aloud
|
|
calls `Surface`, never `Acknowledge`.
|
|
- **No automatic attention-to-action path.** Digestion may summarise Praxis and
|
|
may not call Hexis.
|
|
- Every cross-service call carries a correlation id minted once per action
|
|
(`withCorrelationID`), a contract version header, and `X-Requested-By: maven`.
|
|
|
|
## Routing
|
|
|
|
**Read `docs/routing.md` before touching `internal/router/` or `queryWalk`.** It
|
|
carries the reasoning, the measurements and every rule's why. A route produces
|
|
two decisions. **Intent** is one of seven values. **Source** is where the answer
|
|
lives and is read on `IntentQuery` alone. Score them separately. The cascade is
|
|
stage 0 grammars, then the routing heads, then the resident model, then the
|
|
classifier. Every stage may decline and the next one answers.
|
|
|
|
- **The classifier is the floor, not dead code.** It answers when the resident
|
|
model is off, absent, or erroring. **Any model error falls through.**
|
|
- **The stage 0 set lives in `router.StageZeroGrammars`**, and both `buildRouter`
|
|
and the eval fixture call it. Add a grammar there, in the right place, and read
|
|
the comment above the line you insert after. Do not restate the list anywhere.
|
|
- **Go's `\b` is ASCII-only** and never fires after a Cyrillic letter. A Russian
|
|
pattern needs an explicit `(\s|[?!.]|$)`.
|
|
- **`PraxisGrammars()` is the only path to Praxis**, not a faster one.
|
|
- **`voice.embedder.heads_path` must never point at `model_path`.** Recall
|
|
depends on the resident e5-small scoring what it scored. Fine-tune a copy.
|
|
Refused at config load since V-692, symlinks included.
|
|
- **Routing traces are retained 14 days**, enforced on write and again on start.
|
|
- **Bump `tokenizerRev` on any change to what `encodeWord` emits**, so a
|
|
tokenizer fix triggers `ReembedAll` the way swapping the model file does.
|
|
- **A new rung in the `runTurn` ladder needs its name in `preRouteLadder`**
|
|
(`cmd/mavend/decisiontrace.go`), or it is missing from the decision record.
|
|
|
|
**`queryWalk` takes query sources out and moves none** (`actions_query.go`). That
|
|
is the safety argument and it is not negotiable. The table's order is
|
|
load-bearing and carries "the owner's data first, then the world".
|
|
`SourceUnknown` is the floor and walks the whole chain. A named destination
|
|
removes only the sources marked `guesses: true`, so a source that looks rather
|
|
than guesses is always asked. **The personal boundary is the one exception and
|
|
it is deliberate.** It guesses, so naming `SourceWorld` drops it. **Only a stage
|
|
0 grammar may drop it** (owner's call, V-666). `queryWalk` reads
|
|
`Decision.SourceAnchored` for the source marked `boundary: true` and no other.
|
|
|
|
Judge a routing change against the classifier and the resident model, since
|
|
those always answer. **Their scores live in `docs/routing.md`, never here.** A
|
|
pair copied into this file goes stale silently. The fixture has changed size
|
|
more than once, so a number compares only to another number on the same
|
|
fixture.
|
|
|
|
## Language: model output and Russian
|
|
|
|
Both contracts are in `docs/language.md`. What must not be broken:
|
|
|
|
- **One parser for model text, `parseResponseMood`** in
|
|
`internal/phraser/parse.go`. Every phrasing path reaches it. Mood is an enum.
|
|
- **The router prompt is a separate contract** over 7 intents, and
|
|
`llm/check_prompt_parity.py` keeps the Go and relabelling copies identical.
|
|
- **Russian words are matched by three mechanisms and no fourth**:
|
|
`internal/lexicon` for closed classes, `internal/morph` for grammar, and
|
|
`cmd/mavend/topics.go` with the embedder for open sets. A regex whose output
|
|
is a fact or a route is the defect. A regex over structured input is not.
|
|
- **Seeds are scoring data.** Editing one moves a recogniser and must be
|
|
re-measured against the `TestONNX*` tests, not eyeballed.
|
|
|
|
## Non-goals and hard constraints
|
|
|
|
Not a nag, not autonomous.
|
|
**The persona is feminine.** Russian self-reference takes feminine forms: `рада`
|
|
not `рад`, `поняла` not `понял`. The owner is male and she speaks to him
|
|
informally. Use "ты", singular, never "вы" or "ваш", and never "он" or "его".
|
|
She talks TO the owner, not about him. Pet names such as "милый" are forbidden.
|
|
The name "Ками" is not. `CheckAddress`, `CheckFeminine` and `CheckCringe` in
|
|
`internal/phraser/eval/checks.go` enforce this, scored by `make eval-phrasing`.
|
|
|
|
**"Never phones home" is deprecated** (owner's call, 2026-07-31). She reads
|
|
external sources, and `docs/world.md` carries that chain. What holds regardless:
|
|
|
|
- **No telemetry, no cloud model, no third-party account.** Inference stays on
|
|
the box and nothing about Maven is reported to anyone.
|
|
- **The owner's data first, then the world.** Every source reading his facts,
|
|
notes, calendar, tasks or house runs first, and the personal boundary sits
|
|
between them and anything outside.
|
|
- **His notes and facts are never search input.** Only the utterance goes out,
|
|
never the persona block, the history, or matched notes.
|
|
- **External search is allowed and off unless configured.** Deleting the
|
|
`search` block in `deploy/mavend.json` turns it off.
|
|
- **`Response.Empty()` is the whole gate** on a world answer. There is no
|
|
quality threshold in front of it and four candidate signals all failed.
|
|
|
|
## Session workflow
|
|
|
|
`docs/workflow.md` carries the five stores, the doc tiers and the guards. One
|
|
task, one session, one PR. `/pickup` opens a session and `/wrap` closes it. Wrap
|
|
at roughly half context rather than letting the session compact.
|
|
|
|
```sh
|
|
task start <vikunja-id> # branch off origin/master, write TASK.md, fetch review comments
|
|
task pr # push, open or refresh the PR, label Vikunja, notify
|
|
ToolSearch("select:mcp__vikunja__list_tasks,mcp__vikunja__get_task_details,mcp__vikunja__create_task,mcp__vikunja__update_task")
|
|
```
|
|
|
|
- This repo is Vikunja project **Maven** (ID 2), MCP at
|
|
`http://localhost:9100/mcp`, or `http://192.168.1.104:9100/mcp` from workpc.
|
|
- **A session with no task id asks for one before it starts**, because work
|
|
without one is work nobody can resume.
|
|
- **Close a finished task with `done: true` and nothing else** (owner's call,
|
|
2026-08-07). `update_task` carrying a `description` resets `done` to false.
|
|
- **`pre-commit` refuses master** and more than 300 changed lines in
|
|
non-markdown files. Markdown is exempt and may land as one batch.
|
|
- **`commit-msg` requires the subject to end with `(V-<id>)`.** `V-` and not
|
|
`#`, because Gitea autolinks `#123` to the wrong tracker.
|
|
- **`diff-budget.sh` blocks edits past 600 changed lines** on a `task/` branch.
|
|
- **`--no-verify` exists.** Using it means saying why in the commit body.
|