50130cdffb
490 lines still loads into every session, and most of them explained a
subsystem rather than constraining an agent. The owner's cap is 200. This
lands at exactly 200.
Four new living docs take what left:
docs/deployment.md the two boxes, the resident model, the embedder, STT,
the daemon table, who is in compose, the voice wire,
mavwaked on workpc, the web UI conventions
docs/world.md what replaced "never phones home", why Response.Empty()
is the whole gate, the timeouts, Kiwix
docs/language.md the LLM output contract and the three Russian mechanisms
docs/workflow.md the five stores, the doc tiers, Vikunja, the guards
CLAUDE.md keeps the pointer table and the rules. Every "do not do X", every
path and every owner's call stayed. What went is the before-and-after
narrative behind each one, which is what a living doc is for.
Verified rather than trusted. Every backticked literal in the old file was
diffed against the union of the new ones. Twenty-four came up missing and
three groups were facts rather than narrative, so they were restored:
- the ecosystem client table (nexusClient, praxisClient, the vendored hexis
client, the three config keys and their default URLs) into
docs/ecosystem.md, which did not carry it
- TestOnlyAGrammarMayDropTheBoundary and TestNamingRecallKeepsTheBoundary
into docs/routing.md, since they pin the boundary rule in both directions
- the ipc.Dial vs voice.Dial trap and docs/plans/17 into docs/deployment.md
diff-budget.sh blocked on the changed-line count again. It counts markdown,
which the repo's own pre-commit hook exempts, and this commit touches
nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
201 lines
11 KiB
Markdown
201 lines
11 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 |
|
|
| `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
|
|
```
|
|
|
|
## 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.**
|
|
- **`baselineGrammars` in `eval_test.go` mirrors `buildRouter`.** A grammar
|
|
added to one belongs in both, or the fixture scores a set nobody runs.
|
|
- **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.
|
|
- **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 (76.0% intent, 36.4% destination)
|
|
and the resident model (80.2% intent), since those always answer. The fixture
|
|
has grown from 77 cases to 96, 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.
|