Files
Maven/SPEC.md
T
kami 5fe8f228c1 feat(mavweb): /ecosystem page consuming Nexus/Praxis/Hexis + shell fixes
Add a read-only /ecosystem page that consumes the sibling services'
JSON APIs (Nexus entities, Praxis attention, Hexis capabilities),
fetched concurrently with honest per-panel error states. Siblings stay
headless — mavweb is their human surface (arch §16). Wired via mavweb
-nexus/-praxis/-hexis flags; mavweb joins the ecosystem compose network.

Fix mobile horizontal overflow across all pages: .content is a flex
child with default min-width:auto, so it refused to shrink below the
tables' intrinsic width. min-width:0 lets wide tables pan inside .scroll
instead of dragging the page sideways. Verified via CDP geometry check
(scrollWidth === clientWidth at 430px).

Also includes in-progress Ethos UI redesign, ecosystem deploy compose,
and planning docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:04:23 +04:00

12 KiB
Raw Blame History

Maven — Project Spec

Generated 2026-07-03 from a live QA session. This is the north star, not a roadmap. Priority is: core works → add capabilities → harden. Details change; the principles and target state are settled.

Identity

Maven — self-hosted personal assistant. One daemon on homesrv, multiple client surfaces. All local, never phones home.

Primary name is "Maven" with feminine-gendered Russian self-reference ("она", "меня", "помогла"). Clients are free to choose their own UI label.

Users

Phase Users Data model
Now (MVP) just me Single-user, no namespace
Soon me + gf Per-user namespace (facts/notes/reminders partitioned by speaker attribution)

Per-user means: when the router attributes an utterance to user X, writes go into X's partition. Reads are user-scoped too. Shared state (house chores, shared calendar busyness) is explicitly cross-partition via a shared or household namespace. The router owns attribution — speaker recognition (for voice) plus surface ownership (for text).

Voice pipeline

Real STT + TTS are already wired and tested. The stub floor exists for CI and for the "no models on disk" bootstrap.

Component When active Module Handler
STT voice.stt.socket in config cmd/mavsttd -model <path> whisper.cpp (CGo, Vulkan)
TTS voice.tts.socket in config cmd/mavttsd -piper <bin> -model <path> piper (subprocess, espeak-ng)
STT stub socket unset or module without -model in-process stt.Stub or mavsttd stub hash + template
TTS stub socket unset or module without -piper in-process tts.Stub or mavttsd stub 200ms tone

The server has iGPU + Vulkan. whisper.cpp already uses Vulkan; piper uses CPU (lightweight, real-time). Stubs let the daemon exercise end-to-end without any model files.

Resident language model

One Qwen3-1.7B llama-server process serves both the grammar-constrained route contract and the persona/response contract. The target artifact is produced by RU continued pretraining followed by joint persona/router SFT. Stage-0 grammar, the classifier and stub phrasing remain availability fallbacks. A larger on-demand reasoner and custom TTS training are deferred until the main feature set is complete.

Auth model

A cascade, not a pick-one:

Layer Question Mechanism Surface
0 — network On the tunnel? WireGuard everything
1 — device Enrolled box? mTLS (long-term, optional) PC client
2 — session You, now? Passkey / WebAuthn authed surface
3 — step-up You, right now, for this act? Passkey user-verification gesture destructive tool confirm, registration enable, cold-start unlock

Week 1 floor: layer 0 only (WG tunnel), FloorEnrollment grants full L3 to any same-uid caller. The passkey layer is the deferred build — the code has the auth scaffolding (internal/auth/), just not the WebAuthn dance.

Key invariant: voice/chat structurally cannot reach layer 3. A room mic is reachable by anyone present → destructive acts always gate behind an authed surface for final confirm.

Away-channel fallthrough

When no voice session is active and a nudge fires:

Severity Voice available No voice
sev1-2 (care) voice drop (silent, non-critical)
sev3 (ops soft) voice ntfy
sev4 (ops hard) voice + ntfy telegram, repeat-til-ack

The current code returns ErrNoSession and stops. Target: the dispatcher falls through the routing table to the next channel when voice returns no-session, matching the table above. The voicesink and dispatcher need this reroute path wired.

Calendar

Integration with Radicale (self-hosted CalDAV). Not Nextcloud.

Scope: read + write events:

  • Read: detect busy/available (gate nudges), answer "what's on my calendar"
  • Write: "schedule a meeting at 3pm", "move the dentist appointment"

The calendar_busy config fact already exists and the loop gate reads it. The feed is a new cmd/mavcaldav module (separate binary) that polls Radicale and writes calendar_busy + events as facts through CoreAPI.

Deployment

Phase Mechanism Notes
Now scripts (start-maven.sh, kill-maven.sh) manual start/stop in tmux
Soon systemd user units one per binary, socket-activated modules
Future Docker / Podman single compose, or dockerfiles per component

Invariant: core is rarely redeployed, components are. The IPC boundary (internal/worker STT/TTS sockets, internal/ipc CoreAPI socket) means mavsttd, mavttsd, mavpoll, mavweb can restart independently without touching the daemon. systemd After=mavend.socket, Restart=on-failure per module.

Client protocol

The voice wire protocol (JSON length-prefixed frames over TCP) is designed for multiple client implementations. The reference PWA at cmd/mavweb is one client — any app (phone, desktop CLI, smartwatch) can implement the same frame protocol.

The protocol needs a published spec document so third parties can write clients without reading the Go source. Spec document lives in PROTOCOL.md and covers:

  • Transport: TCP, length-prefixed JSON frames (4-byte big-endian length)
  • Methods: PushToTalk, Pong
  • Push kinds: AudioNudge
  • Surface identity: header field, cap enforcement server-side
  • Error codes

The auth section of the spec documents how passkey assertions are carried over the wire (for the step-up layer).

Multi-user architecture (gf phase)

When the second user arrives:

  1. Speaker attribution — the router produces a speaker label per utterance ("kami", "gf", "unknown"). Voice uses speaker embedding / voice-print match; text/telegram uses explicit surface ownership or a command prefix.
  2. Per-user partitioningfacts.user_id, notes.user_id, reminders.user_id. Queries scope to the current speaker's partition.
  3. Cross-user reads — explicit, e.g. "show kami's calendar" or "remind us both". The router decides from the utterance form.
  4. Shared namespace — household chores, shared calendar, home automation. A user_id = 0 or user_id = 'shared' convention.

This is a post-MVP concern. Single-user works for now; the schema has no user_id columns yet. Adding them later is a migration, not a rewrite, because the append-only design means no existing row needs updating.

ML & hardware profile

Resource Available Used by
CPU Ryzen, 13GB RAM loop, router classifier, delivery
iGPU Vulkan-capable whisper.cpp (STT), piper (TTS)
GPU layers llama-server with -ngl -1 Qwen3-Maven-1.7B resident model

Models are per-component, downloaded separately (gitignored models/ dir). No model is baked into the binary.

Open design items

Priority: core works → add capabilities → harden. Items 16 are capabilities; 7 is hardening; 8 is post-MVP and explicitly out of scope this phase.

Each item includes a "done when…" line so an autonomous agent has a checkable finish criterion.


1. Protocol spec (PROTOCOL.md)

Document the voice wire format so the protocol is multi-client by design. Must be generated from internal/voice/wire.go, not composed freehand — the wire.go comments are already complete; the spec must not drift from code.

  • Transport: TCP, length-prefixed JSON frames (4-byte big-endian length)
  • Methods: PushToTalk, Pong
  • Push kinds: AudioNudge
  • Surface identity: header field, cap enforcement server-side
  • Error codes

Done when: a new client implementor can build a working PushToTalk round-trip from PROTOCOL.md alone, and diff PROTOCOL.md internal/voice/wire.go shows no contradictions in constants or method names.


2. Away-channel fallthrough

Wire the dispatcher to fall through when voice returns ErrNoSession, matching the table in § Away-channel fallthrough (lines 6771).

The voicesink and dispatcher need this reroute path wired.

Done when: a test (or manual trace) where voice returns ErrNoSession and a sev3 nudge lands on ntfy, sev4 on telegram-repeat-til-ack, sev12 drops silently. Existing delivery test patterns in internal/delivery/ show the shape.

Note: the store.Away routing path is already built and tested (TestDispatchNudgeOpsHardAwayTelegramRepeatUntilAck passes). The gap is the runtime ErrNoSession path — when the dispatcher chose voice (severity table said voice was available) but no session is live at push time. voicesink.go:62 has a TODO for this. Wire the fallthrough onto the same routing table the Away path uses; converge, don't duplicate.


3. CalDAV poller (cmd/mavcaldav)

New separate binary that polls Radicale (CalDAV) on a configurable interval and writes facts through CoreAPI:

  • calendar_busy — boolean, read by the loop gate
  • calendar_event — per-event facts for "what's on my calendar" queries

Writes only on value change (same append-only discipline as mavpoll).

Done when: cmd/mavcaldav -socket <core> -url <radicale> -user <u> -pass <p> runs, polls Radicale, and a calendar_busy fact with the right value appears in the store. A DueReminders-style test proves the loop gate reads it.


4. Quiet-hours schedule

The loop reads a quiet_hours config fact. Today a voice toggle writes it manually ("тихий режим"). Target: the fact is also set by a time-window schedule (e.g. "quiet from 23:00 to 08:00") or automatically from calendar-busy. The schedule lives in config (voice.quiet_hours_window or similar); the loop writes the config fact at tick boundaries when the window is active.

Done when: setting a quiet window in config suppresses proactive nudges during those hours without the user saying "тихий режим", and calendar-busy also gates the same way.


5. Tool executor /tools page

The executor + matcher are wired (internal/tool, internal/store/tools.go). Tools are proposed via voice, but enabling them requires an authed surface. The missing piece is the /tools page at cmd/mavweb serving enable/ disable/discover UI, gated at AuthStepUp.

Done when: visiting /tools on mavweb shows proposed tools with an "Enable" button, and enabled tools with "Disable". Enabling fills cmd + destructive flag and writes to the store. The tool runs on the next matching utterance without a daemon restart (already true — store-backed).


6. Note RAG

Today the query intent returns the verbatim top-k note. Target: feed gated top-k notes to the phraser (llama-server) to compose a natural answer: "вот что я нашла:

".

Done when: asking "что я говорил про X" returns a phrased answer with content from the matching notes, not a raw note dump.


7. Passkey step-up

WebAuthn enrollment + assertion in the authed surface. Replaces FloorEnrollment with real passkey verification for:

  • Destructive tool confirm (layer 3)
  • Registration enable (layer 3)
  • Cold-start unlock (layer 3)

Done when: a destructive tool requires a WebAuthn gesture (biometric/PIN) before it fires; FloorEnrollment is removed; cold-start passes through the passkey page.


8. Multi-user schema

DO NOT TOUCH THIS PHASE. Per-user partitioning (facts.user_id, notes.user_id, reminders.user_id) is a migration-later concern. The schema has no user_id columns; adding them when the second user arrives is a migration, not a rewrite, because append-only means no existing row needs updating. An autonomous agent must not introduce user-scoping mechanisms while single-user is the only operational mode.

Non-goals (unchanged from maven.md)

  • Not a relationship simulator
  • Not a guesser of truth — inference changes whether she asks, never what she records
  • Not a nag — would rather miss a nudge than be mutable
  • Not autonomous — suggests and acts on command, never unsandboxed action rights