# Spec: every capability, and what done means for each *Last verified: 2026-08-15 @ 231248a* What Maven is for, capability by capability, with a definition of done that can be observed on the running box. `CLAUDE.md` carries the rules. `docs/roadmap.md` carries the order. This file carries the target. The capability list is the union of two sources. The 39 rows measured in `docs/evals/2026-08-13-capability-audit.md`, and the 18 items the owner named as v1 scope. Twelve of the owner's items had no audit row, so this file has 51. Five entries are marked deferred past v1. They keep their DoD, so it is written when one comes back. ## How to read an entry **State** is a reference, never a word. It points at the living doc that holds the current or the desired implementation. Where no such doc exists, the entry says so, and that absence is a finding this file is surfacing on purpose. **DoD** is a plain list of observable conditions. Each item is checkable on the running stack. An utterance and its answer, a log line, an outbox row, an HTTP response, or a store row. **Scenario** names a file in `cmd/mavend/testdata/scenarios/`, which scopes the flow from utterance to output. Five exist today: `act_degraded`, `assistant_workday`, `conversation_anaphora`, `evening_degraded`, `morning_missed`. A name marked *(to write)* does not exist yet, and writing it is part of that capability's work. Every clock time in this file is `Europe/Samara`, declared once in `docker-compose.yml` with `/etc/localtime` pointed at it (V-545). Every path reads `time.Local`. ## What v1 means v1 is a voice assistant, minimum viable. Voice is the spine and the capability list is the surface. Each DoD below is written at "voice-reachable and honest", not at "feature-complete". Honest means she names the gap when she cannot do the job, and never fills it with a guess. Seven capabilities in v1 have no design yet. They keep a DoD here, written at what done would look like, and their state entry names the missing doc. The gap stays visible instead of implied-done. ## The turn ### Route an utterance - **State**: `docs/routing.md`. - **DoD** - Seven intents score separately from the source, on every turn. - A question about what Maven remembers routes to query, not remember. The audit's two misrouted probes of seven pass. - Any resident-model error falls through to the classifier and the turn still answers. - Every turn writes a decision trace naming the stage that decided. - **Scenario**: `conversation_anaphora`, `assistant_workday`. ### Ask instead of guessing - **State**: `docs/routing.md`, the clarify head and the parked clarify ride. - **DoD** - An incomplete reminder parks and asks for the missing slot, not for all of them. - "отмена" during a parked clarify drops the parked turn and says so. - A parked clarify survives an interleaved unrelated turn and resumes. - **Scenario**: `conversation_anaphora`. ### Speak as herself - **State**: `docs/language.md`. - **DoD** - No masculine self-reference reaches the wire. The audit caught "Хорошо, сохранил" live while `CheckFeminine` passed in the eval, so the check runs on the outbound path and not only in the eval. - She says "ты" throughout, and no pet name appears. - A rejected reply is logged with what failed, not silently rephrased. - **Scenario**: `morning_missed` pins the constant. `persona_wire` *(to write)* pins model output on the outbound path. ### Answer from your own data - **State**: `docs/routing.md`, `queryWalk` in `cmd/mavend/actions_query.go`. - **DoD** - "что у меня сегодня по плану?" returns the real checklist and its open items. - The owner's sources are asked before anything outside, every time. - A source that looks rather than guesses is asked even when a destination is named. - **Scenario**: `assistant_workday`, `morning_missed`. ### Answer from the world - **State**: `docs/world.md`. - **DoD** - A general-knowledge question returns a Russian summary that does not invent physics. The audit's "почему небо голубое?" answer is the failing case. - Only the utterance leaves the box. No persona block, no history, no matched notes. - Deleting the `search` block turns the capability off with a named gap, not an error. - **Scenario**: `world_summary_quality` *(to write)*. ### Read an encyclopedia - **State**: `docs/world.md`, the Kiwix section. - **DoD** - A Russian question lands on the Russian book and an English one on the English book. - Kiwix answers when the line is down. - The retrieved article is on the question's topic, not merely a lexical match. - **Scenario**: `kiwix_language_pick` *(to write)*. ### Weather - **State**: `internal/weather`. No living doc covers it. **Finding**: the provider seam, the home city and the clarify path have no written reasoning anywhere. The audit found the capability broken on configuration alone. - **DoD** - A `weather` block in `deploy/mavend.json` names a provider and a home city. - "какая сейчас погода?" answers for the home city without asking. - Naming another city answers for that city. The audit's follow-up "Самара" died with "Я тебя не разобрала", so the follow-up parks as a clarify instead of emitting a question through the answer path. - With no provider configured she names the gap and does not guess a forecast. - **Scenario**: `weather_followup` *(to write)*. ### See an image - **State**: `internal/vision`, the seam that stores the image and says so. No living doc. **Finding**: V-667 has the gemma-4 mmproj on the box and no written contract for what a vision call returns. - **DoD** - An image sent through Telegram gets a Russian description. - With no vision model configured she says she cannot look, and the image is stored. - The vision call goes to the workstation and falls back silently when it is down. - **Scenario**: `vision_degraded` *(to write)*. ## Memory The four memory rows below have no living doc. **Finding**: `docs/design.md` sketches the store and `docs/routing.md` covers recall's routing. Nothing owns the fact and note contracts, the supersede rule, or the digestion worker's consolidation pass. This is the largest documentation gap in the list. ### Facts - **State**: `internal/store/facts.go`, `cmd/mavend/factenrichment.go`. No living doc. - **DoD** - A stated fact is written and confirmed in his own words, in the feminine. - A confirmation that says she wrote something is never emitted without the row existing. The audit's "я записала информацию о тебе" wrote nothing. - A superseding fact retires the old value and both are readable. - `actionFact.Subject` resolves through Nexus, never through a local key. - **Scenario**: `morning_missed`. ### Notes - **State**: `internal/memory`. No living doc. - **DoD** - A note captured from any reach is recallable by question. - A note can be deleted by voice and from the web UI. `/api/revert` voids facts by key and nothing voids a note today (V-494). - A question is not stored as a statement. The audit's "я рассказывал тебе про байкал?" became a junk note. - **Scenario**: `note_delete` *(to write)*. ### Recall - **State**: `docs/routing.md` for the query walk and the personal boundary. - **DoD** - The embedder loads at 384 dimensions with the marker check passing, on every start. - `EmbedQuery` and `EmbedPassage` carry their prefixes. A plain `Embed` on a note fails the build or the test. - The personal boundary scores a question about him as personal. `TestONNXPersonalBoundary` is green. - A recall miss says she does not remember, and does not answer from the world instead. - **Scenario**: `assistant_workday`. ### Memory evaluation - **State**: `internal/memeval`. No living doc. **Finding**: the evaluator ships, writes notes and cannot speak, and nothing records what its conclusions mean (V-248). - **DoD** - One evaluation run is observed on the box and its notes are read back. - What it concluded is checkable against the notes it read. - **Scenario**: `memeval_run` *(to write)*. ## Proactive ### Reminders - **State**: `internal/store`, `internal/delivery`. No living doc covers the reminder lifecycle. **Finding**: parking, firing, delivery, retry and cancellation are spread across three packages with no written contract. - **DoD** - A one-shot reminder set by voice fires at its time and is delivered. - **Recurring works from speech**: meetings, pills, the dog, the vet and the kibble. `store.Reminder` carries `Cron` and `ipc.CreateReminder` takes a cron argument, and no caller in `cmd/mavend` passes one. Recurring is unbuilt with its storage and delivery already finished under it. - A recurring reminder states its schedule back when it is set, and again when asked. - Cancellation works by voice and from `/reminders`, and a refusal is honoured. - A failed delivery retries into another reach rather than looping. The audit watched the ntfy failure run once a minute until 03:05. - **Scenario**: `recurring_reminders` *(to write)*. Cancellation is covered by the V-719 eval. ### Interruption policy - **State**: `docs/handler-wiring.md` for the dispatch decision. **Finding**: the four presence-and-severity outcomes have never been written down as intended behaviour, only as code (V-281). - **DoD** - The four outcomes are named in a doc before any of them changes. - A severity-1 item with him present at the desk reaches him through some reach. The audit logged `dropped morning:утро (sev1, presence=present)`. - Nothing unprompted arrives during a quiet tick. - **Scenario**: `morning_missed`, `evening_degraded`. ### Digest of held nudges - **State**: `internal/worker`, the digestion worker. No living doc. - **DoD** - A suppressed nudge candidate is observed surfacing in a later digest, on the box. - The semantic fingerprint is checked before the phraser is paid. - Digestion never calls Hexis. - **Scenario**: `evening_degraded`. ### Morning routine - **State**: `internal/morning`, `internal/routine`. No living doc. - **DoD** - The morning plan reaches him inside its 08:00-11:00 window, `Europe/Samara`. - When the voice reach has no session the plan falls back to the non-voice reaches rather than being dropped (owner's call; related V-281). - A missed window is stated as missed, not silently swallowed. - **Scenario**: `morning_missed`. ### Routine proposals - **State**: `internal/routine`. No living doc. **Finding**: the proposer reads a hand-written Russian verb list, which the language rules forbid as a route or fact source (V-606). - **DoD** - One proposal is observed on the box from real repeated behaviour. - The verb list is replaced by `internal/lexicon` or the embedder. - A proposal is a nudge he can decline, and declining it is stored. - **Scenario**: `routine_proposal` *(to write)*. ### Tasks - **State**: `internal/tasks`. No living doc. - **DoD** - Open tasks are read back ordered by deadline and urgency. - A task captured by voice appears in the list and on the web UI. - Mail-derived candidates are never spoken as tasks until he accepts one (V-130). - **Scenario**: `assistant_workday`, `morning_missed`. ### RSS and news - **State**: `internal/rss`. No living doc. - **DoD** - Feed items are read when asked and never announced unprompted. That is the intended shape, not a defect. - A question about a topic finds the matching item across the configured feeds. - A dead feed names itself as dead, and the others still answer. - **Scenario**: `morning_missed`. ## Reach ### Telegram - **State**: `docs/deployment.md`, `internal/delivery/telegramsink`. - **DoD** - An outbound message is delivered and the outbox row records the delivery. - His chat is read continuously from restart. - The socks relay being down names the gap and holds the message. - **Scenario**: `evening_degraded`. ### ntfy - **State**: `internal/delivery/ntfysink`, disabled in the committed config. - **DoD** - A write-scoped `NTFY_TOKEN` exists before re-enabling. It was switched off after a 403 storm. - A 403 stops retrying instead of looping once a minute. - **Scenario**: `ntfy_403` *(to write)*. ### Voice - **State**: `docs/protocol.md` for the wire, `internal/delivery/voicesink` for the sink. **Finding**: the wire is documented and the listener is not. Nothing describes what holds a live voice session open. - **DoD** - A proactive message reaches him by speech without him speaking first. Every proactive message during the audit fell through with "no live voice session". - The port stays on homesrv loopback and reaches workpc over ssh. - `SurfaceVoice` caps acts at L0, and reading is not capped. - **Scenario**: `voice_push` *(to write)*. ### Web UI - **State**: `docs/deployment.md`. - **DoD** - All pages answer 200. - Every capability with a surface has a page: reminders, notes, tasks, facts. - A destructive action on a page is gated by step-up. - **Scenario**: covered by `cmd/mavweb` tests, not by a scenario. ### Desk notifications - **State**: `cmd/mavweb/ambient.go`, `internal/event`. No living doc. **Finding**: the inbound direction exists as the `ambient:notif` source and the outbound direction does not exist at all. Which one the owner means is an open product decision. - **DoD** - Inbound: a desktop notification becomes a fact at the ambient path's own confidence, filed low, and never spoken back unprompted. - Outbound: a nudge can appear on the workpc desktop as a fourth reach, or the outbound half is explicitly dropped from v1. - **Scenario**: `morning_missed` covers inbound. Outbound has none. ## Speech and senses ### Speech to text - **State**: `docs/offload.md`, `docs/deployment.md`. - **DoD** - Russian speech transcribes accurately enough to route. `mavsttd` is the local floor and the workstation transcriber is the better path. - The workstation being down falls back to `mavsttd` silently. - The floor arm is exercised on its own, not only behind the workstation. - **Scenario**: covered by `docs/evals/2026-08-09-crisperwhisper2-russian-wer.md`. ### Text to speech - **State**: `docs/offload.md`, `docs/deployment.md`. - **DoD** - A reply is spoken in Russian with correct number and abbreviation expansion. - `internal/ttsnorm` expands times and dates into `Europe/Samara` phrasing. - **Scenario**: `tts_normalisation` *(to write)*. ### Wake word - **State**: `docs/deployment.md`, `mavwaked` under systemd on workpc. - **DoD** - "Мэйвен" wakes her and a near-miss does not. - Waking her opens a voice session the proactive path can push into. Waking is not the same as having a session (V-515). - **Scenario**: `voice_push` *(to write)*. ### Hearing - **State**: `internal/capture`, `internal/audio`. No capture client ships (V-514). - **DoD** - A capture client runs on workpc and streams to `mavsttd` or the workstation. - The path is reachable end to end from microphone to reply. - **Scenario**: `voice_push` *(to write)*. ### Speaker recognition **Deferred past v1**, owner's call 2026-08-15. See `docs/roadmap.md`. - **State**: `internal/speaker`. No living doc (V-255). - **DoD** - The owner's voice is distinguished from another voice. - A voice that is not his cannot reach the act path. - **Scenario**: `speaker_gate` *(to write)*. ## The ecosystem ### Nexus - **State**: `docs/ecosystem.md`. - **DoD** - Entities exist. Nexus answers "no entities yet" today, so every act naming a target has nothing to resolve against. Seeding is a Nexus-side job. - Free text resolves to a canonical entity id before any mutating call. - Ambiguous resolution asks him and does not pick. - Nexus down produces a named gap, not a broken turn. - **Scenario**: `act_degraded`. ### Praxis - **State**: `docs/ecosystem.md`. - **DoD** - An item needing attention is read back on request. - Reading an item aloud calls `Surface`, never `Acknowledge`. - Attention arrives over HTTP, never from its SQLite file. - Digestion may summarise Praxis and may not call Hexis. - **Scenario**: `morning_missed`, `evening_degraded`. ### Hexis - **State**: `docs/ecosystem.md`. - **DoD** - An act runs against a real target once Nexus has entities. - Confirmation binds capability id, target entity, arguments, requester and expiry. - LLM output alone never authorizes. - Every call carries a correlation id, a contract version and `X-Requested-By: maven`. - **Scenario**: `act_degraded`. ### Smart home **Deferred past v1**, owner's call 2026-08-15. See `docs/roadmap.md`. - **State**: `internal/smarthome`, disabled in config (V-256). - **DoD** - A device is controlled through Hexis, resolved through Nexus, never by free text. - Disabled means a named gap. - **Scenario**: `act_degraded`. ### Network scans - **State**: `internal/netscan`, `internal/netaddr`. No living doc. - **DoD** - A scan of the configured subnets returns hosts and open ports on request. - The result is read back as prose, not as a table dump. - The rate limit in the config is honoured. - **Scenario**: `netscan_query` *(to write)*. ### Bluetooth control **Deferred past v1**, owner's call 2026-08-15. See `docs/roadmap.md`. - **State**: no package. **Finding**: nothing exists, and the box has no bluez (V-257). This is the only v1 item blocked on the host rather than on code. - **DoD** - bluez is present on the box that owns the radio. - A paired device is connected and disconnected by voice, through Hexis. - **Scenario**: `bluetooth_control` *(to write)*. ### MCPs - **State**: `internal/mcp`. No living doc. **Finding**: the allowlist, the stdio and http transports and the webfetch door all exist. Nothing records which servers may run, or why. - **DoD** - A configured MCP server's tools are callable through the act path. - The allowlist is the only path to a tool, and a tool outside it is refused. - A server that dies is a named gap, and the rest of the registry still answers. - **Scenario**: `mcp_tool_call` *(to write)*. ## Operations ### The deployed stack - **State**: `docs/deployment.md`. - **DoD** - All five compose services run the current build. The audit found three on a four-day-old image. - `mavwaked` on workpc is on the current build too, or its drift is stated. - A restart loses nothing. - **Scenario**: none. This is checked by `docker compose ps` and the startup log. ### Encrypted database - **State**: `docs/deployment.md`, `docs/caveats/storage.md`. - **DoD** - The database is encrypted at rest with the working copy in tmpfs. - The key comes from the environment and is held only by `mavend`. - Passwords are read from files, never taken as flag values. - **Scenario**: none. Checked by `mavseal` and the startup log. ### Passkey and step-up - **State**: `docs/caveats/security.md`, `internal/webauthn`, `internal/auth`. - **DoD** - WebAuthn is configured, so no step-up gate is fail-open. Today every one is, including `POST /api/chat`, which reaches the act path (V-683). - Enrollment requires an existing credential once the first one exists. - Step-up is per-request, not process-global. - **Scenario**: `stepup_gate` *(to write)*. ### Model swap **Deferred past v1**, owner's call 2026-08-15. See `docs/roadmap.md`. - **State**: `docs/deployment.md` (V-250). - **DoD** - `phraser.swap_models` lists the allowed gguf paths, or the page is removed. - A swap survives a restart, or the page states that it will not. - **Scenario**: none. ### Self-update **Deferred past v1**, owner's call 2026-08-15. See `docs/roadmap.md`. - **State**: `cmd/mavupdate`, `internal/update`. Blocked at step 3: it cannot reach the containerized socket (V-477). - **DoD** - An update runs to completion from inside the deployment. - A failed update rolls back and says so. - **Scenario**: none. ### Tests and analyzers - **State**: `docs/qa.md`, `docs/workflow.md`. - **DoD** - `make test` is green with `-race` and `MAVEN_ONNX_LIB` set, so the four `TestONNX*` measurements run instead of self-skipping. - `make analyze` passes against its baselines, and a fix deletes its entry. - The one latency test that fails only under coverage is fixed or filed (V-718). - **Scenario**: none. This gate is the suite itself. ## Undesigned in v1 These seven have no design and no living doc. Each keeps a DoD written at what done would look like, so the gap is visible instead of implied-done. A design pass comes before any of them is built. ### Email triage - **State**: `internal/email`, `cmd/mavmaild`. Built and **not in `docker-compose.yml`**. **Finding**: the product decision comes first. What she does with his mail is undecided, and deploying the daemon before deciding writes the decision by accident. - **DoD** - The owner has decided what triage means: read-only summary, task extraction, or reply drafting. - Mail-derived task candidates stay candidates until he accepts one (V-130). - No mail content leaves the box. - `mavmaild` is in compose, or its absence is deliberate and recorded. - **Scenario**: `email_triage` *(to write)*. ### Calendar management - **State**: `internal/calendar`, `cmd/mavcaldav`. Built and **not in `docker-compose.yml`**. Same product decision as email. - **DoD** - Today's and tomorrow's events are read back, times in `Europe/Samara`. - An event is created by voice, with the slot asked for rather than guessed. - A conflicting event is stated as a conflict. - `mavcaldav` is in compose, or its absence is deliberate and recorded. - **Scenario**: `calendar_create` *(to write)*. ### Web crawling - **State**: `internal/crawl` with `robots.go` and `watch.go`, `internal/webfetch`. No living doc. **Finding**: politeness and robots are implemented. The scheduling policy is not written anywhere. - **DoD** - A page is fetched on request and summarised in Russian. - A watched page reports what changed, on a schedule he set by voice. - robots and the politeness delay are honoured, observable in the log. - Only the URL and the utterance leave the box. - **Scenario**: `crawl_watch` *(to write)*. ### Summaries - **State**: no package. **Finding**: summarisation exists inside the world chain and inside digestion, and nothing owns it as a capability he can ask for. - **DoD** - "перескажи" over a note, a feed item, a page or a mail returns a Russian summary. - The summary names its source. - A summary that would invent content is refused, the way `Response.Empty()` gates a world answer. - **Scenario**: `summarise` *(to write)*. ### Webhooks - **State**: only `internal/delivery/telegramsink/intake.go`, which is Telegram's own inbound webhook. **Finding**: there is no general webhook capability in either direction, and no doc says which direction is wanted. - **DoD** - Inbound: an authenticated external event becomes a fact or a nudge candidate. - Outbound: a nudge can post to a configured URL as a reach. - Neither direction is reachable without authentication. - **Scenario**: `webhook_inbound` *(to write)*. ### Cron jobs - **State**: `internal/routine`, `cmd/mavend/tick_routines.go`. **Finding**: routines carry a `Cron` and are a separate mechanism from reminders. Whether "cron jobs" means user-defined scheduled acts or the existing routines is undecided. - **DoD** - A scheduled job is created by voice, with its schedule stated back. - It runs on schedule in `Europe/Samara` and its run is recorded. - A job that runs an act is bound by the same confirmation rules as any act. - Its relationship to routines and to recurring reminders is written down, so three schedulers do not exist. - **Scenario**: `cron_job` *(to write)*. ### Learning the style - **State**: no package. **Finding**: nothing exists beyond `internal/phraser/eval/checks.go`, which scores style and does not learn it. Learning means behavioral, not weights: stored outcomes, no adapter, no training set. - **DoD** - A corrected phrasing is stored as an outcome and changes a later reply. - No model weights change and no training set is built. - What was learned is readable on a page and can be deleted. - **Scenario**: `style_correction` *(to write)*. ### Learning from mistakes - **State**: no package. Same behavioral rule as above. - **DoD** - A dismissed nudge, a corrected phrasing and a repaired route are each stored as an outcome. - A repeated dismissal suppresses that nudge shape. - A repaired route changes the next routing of the same utterance. - Every stored outcome is readable and deletable. - **Scenario**: `learn_from_dismissal` *(to write)*. ### Command chaining - **State**: no package. The `chain` in `internal/router` is the world chain and the source chain, not command chaining. **Finding**: nothing exists. - **DoD** - "напомни мне и запиши это" performs both, or asks which one. - A chain containing an act confirms each act separately. - A failed step stops the chain and names the step that failed. - **Scenario**: `command_chain` *(to write)*. ## What this file rules out A definition of done that a test suite can score. The suite was green during the audit and 22 of 39 capabilities were not live. Every criterion above is observable on the running box. A roadmap ordered by code work. None of the four broken capabilities is a code defect. Weather has no configuration block. Nexus has no data. The voice reach has no listener. Step-up has no WebAuthn credential. ## The documentation gaps this file surfaced Applying the "state is a reference" rule found seventeen capabilities with no living doc. Memory is the largest cluster. Facts, notes and the digestion worker have no owning document at all. Proactive is the second. The reminder lifecycle spans three packages with no written contract. `docs/roadmap.md` orders the work, and these gaps are part of it.