Files
Maven/SESSION-06-07-2026.md
T

17 KiB
Raw Blame History

Overnight Session — 2026-07-06

Branch: overnight-jul6 (from master) Executor: a single unsupervised agent working through the night.


READ THIS FIRST — Operating rules (do not skip)

You are working unsupervised. Optimize for not breaking anything over finishing every task. A half-finished task that compiles and is committed is a success; a clever half-rewrite that breaks the build is a failure.

Hard rules:

  1. One task = one commit. Never batch two tasks into one commit. Commit message: maven: <task-title> (task N). Sign-off line required (see repo convention — Co-Authored-By trailer).
  2. TDD, always. For every task that touches Go: write the test first, watch it fail, then write code until it passes. Tests live next to the code as *_test.go. Copy the style of the nearest existing test file.
  3. After every task, run the gate before committing:
    gofmt -l .            # must print nothing
    go build ./...        # must succeed
    go vet ./...          # must be clean
    go test ./...         # must be green
    
    If any step fails and you cannot fix it in ~15 min, git stash or revert that task, write a note in the task's Status cell ("BLOCKED: "), and move to the next task. Do not leave a broken tree.
  4. Never invent config keys, function names, or file paths. Every new thing copies an existing pattern named in the task. If you can't find the pattern, mark the task BLOCKED and skip it.
  5. Tools/acts: never add a tool without "destructive": true unless it is provably read-only (see Task 2). A destructive act that runs from voice without a confirm gate is the worst possible bug. When unsure → destructive.
  6. Do NOT attempt the "DEFERRED — needs human" section at the bottom. Those need hardware or protocol decisions. Touching them unsupervised will waste the night. They are listed only so you don't rediscover them.
  7. Prefer additive changes. Do not refactor existing packages. Do not touch cmd/mavweb/, encryption, or the store schema unless a task says to.

Work top-to-bottom. Tasks are ordered by value-per-risk: safest and most self-contained first. If you run out of night, the earlier tasks are the ones that matter.


Key facts about the codebase (so you don't have to rediscover them)

  • Router cascade: internal/router/. Intents are the constants in intent.go (act, reminder, fact, note, query, system). Adding an intent = add a const there + seed examples + a handler case.
  • Intent seeds: models/seeds/<intent>.txt, one example per line, # comments allowed. Loaded by seedClassifier in cmd/mavend/voice.go. To teach the classifier a new phrase, add a line to the right seed file — no code change needed.
  • Voice intent dispatch: cmd/mavend/voice.go, the big switch dec.Intent (search case router.IntentQuery: ~line 412). Each intent returns a Russian reply string. This is where a new intent's behaviour hangs.
  • Tools/acts: enabled allowlist lives in config voice.tools (see internal/config/config.go ToolConfig). Executor: internal/tool/tool.go. Args are argv, never shell. Destructive tools return ErrNeedsConfirm.
  • Config: internal/config/config.go. Seed/prod config: deploy/mavend.json.
  • Store (facts, notes, reminders, tools): internal/store/. CalDAV events are written as facts with source=caldav plus a calendar_busy key (per the poller in mavpoll/mavcaldav).
  • Embedder: voice.embedder config → ONNX; nil → router.NewHashEmbedder floor. Wiring is in cmd/mavend/voice.go ~line 144.
  • Language is Russian. Maven refers to herself in the feminine. All user-facing reply strings are RU. Copy tone from existing replies.

Phase 0 — Setup (do this once, first)

  • git checkout master && git pull (if remote), then git checkout -b overnight-jul6
  • Run the full gate (go build ./... && go vet ./... && go test ./...) on a clean tree to confirm a green baseline before you change anything. If baseline is red, STOP and record it here — do not build on a broken tree.

Task 1 — Embedder config validation + docs (safest, do first)

Goal: make embedder misconfiguration fail loudly instead of silently falling back to the Hash floor.

Files: internal/config/config.go (Validate path), its *_test.go, deploy/mavend.json, and a short note in START.md or PROGRESS.md.

Do:

  1. Find where VoiceConfig / EmbedderConfig is validated (look for a Validate() method or the load path in config.go). Add a check: if Embedder is non-nil, then all three of ModelPath, TokenizerPath, LibPath must be non-empty — a partially-filled embedder block is a config error (return fmt.Errorf(...)). If Embedder is nil, that's fine (Hash floor) — no error.
  2. In cmd/mavend/voice.go around the embedder wiring (~line 144159), make the "falling back to HashEmbedder" path an explicit log.Printf("voice: embedder not configured, using HashEmbedder floor") if it isn't already.
  3. Add a test to config_test.go covering: all-three-set → ok; one-missing → error; nil → ok.
  4. Document the voice.embedder block (all three paths, and "omit the block to use the floor") in START.md near other config docs.

Done when: new test passes, gate green, docs updated. One commit.


Task 2 — Seed the tool allowlist with safe homelab acts

Goal: give the voice act path a useful, SAFE starter allowlist.

Files: deploy/mavend.json (voice.tools), and models/seeds/act.txt.

Do:

  1. Add tools to voice.tools in deploy/mavend.json. Each: name, cmd (argv prefix), scope, destructive. Classify carefully:
    • Read-only (destructive: false) — safe to fire from voice: systemctl status, docker ps, uptime, df, free, journal reads (journalctl -n 50 -u <unit> — note the unit comes as an arg).
    • Destructive: true — must confirm: systemctl restart, systemctl stop, docker restart, reboot, docker stop.
    • When unsure → destructive: true.
  2. Add matching spoken RU phrasings to models/seeds/act.txt (e.g. «покажи статус nginx», «перезапусти nginx», «сколько места на диске») so the classifier routes them to act. One per line.
  3. There is no Go change here if the executor already reads voice.tools. Verify by reading the wiring — if tools are loaded from config into the store allowlist at boot, you're done. If not, mark BLOCKED (don't build new wiring).

Done when: go test ./... still green (config parses), the JSON is valid (go run the daemon far enough to parse, or a small config-load test). One commit.

Guardrail: double-check no restart/stop/reboot/rm/kill entry has destructive: false. This is the single most important check of the night.


Task 3 — Calendar event querying ("что у меня завтра?")

Goal: answer calendar questions from CalDAV facts already in the store.

Files: internal/router/ (slots + a query sub-path, or reuse IntentQuery with a calendar slot), cmd/mavend/voice.go (handler), models/seeds/query.txt, and tests.

Approach (keep it simple — don't add a new intent if you can avoid it):

  1. The data is already there: CalDAV events are facts with source=caldav. Find the store method that reads facts by source/date (grep caldav in internal/store/). If none scopes by date, add a small read helper CalendarEvents(ctx, from, to time.Time) next to the existing facts queries — copy the style of an existing store/facts.go query, with a test.
  2. Add date-scope parsing: «сегодня» → today, «завтра» → tomorrow. Put this in a small helper in internal/router/slots.go (copy the RU parsing style in slots_ru_test.go). Test it directly.
  3. In the IntentQuery handler in voice.go, detect a calendar question (the utterance mentions планы/календарь/завтра/сегодня + no note match, OR a dedicated keyword check before the notes RAG lookup). Read events for the scoped day, format an RU reply: empty → «на сегодня ничего нет», one/many → list them. Keep formatting in a tested pure helper.
  4. Seed models/seeds/query.txt with the example phrasings.

Done when: helper tests + a handler-level test pass, gate green. One commit (or two: store helper, then handler — that's fine, keep them separate).

If store scoping turns out hard: ship just the date parser + formatter as pure tested helpers and wire them to a naive "read all caldav facts, filter in Go" — personal scale, correctness over efficiency. Do not add schema.


Task 4 — General-knowledge routing to the phraser

Goal: route open factual questions to the phraser with an anti-hallucination system prompt and a fallback.

Files: cmd/mavend/voice.go (query handler), phraser call site (grep phraser / Phrase in voice.go and internal/phraser/), models/seeds/query.txt, tests.

Do:

  1. In the IntentQuery handler, after the notes-RAG lookup fails to clear queryMinScore (currently returns «у меня нет заметок…»), instead of giving up, hand the question to the phraser with a system prompt like: «Ответь кратко из своих знаний. Если не знаешь — скажи "не знаю". Не выдумывай.» (feminine self-reference).
  2. Fallback gate: if the phraser returns empty, errors, or the phraser is the Stub (not configured), return «не знаю» / the existing no-answer reply. Never fabricate.
  3. Keep the prompt construction in a small pure function so you can unit-test it (assert the system prompt text + that empty phraser output → fallback).

Done when: prompt-construction test + fallback test pass, gate green. One commit.

Risk note: the phraser is a small model and will hallucinate. The fallback is the point of this task — test it hard. Do not remove the notes-RAG path; this is a fallback after it.


Task 5 — Weather module skeleton (pure, no network at night)

Goal: a pluggable weather provider interface + an unconfigured stub. No live API calls.

Files: new internal/weather/ package, internal/config/config.go (a WeatherConfig block, copy PhraserConfig shape), tests.

Do:

  1. internal/weather/weather.go: define type Provider interface { CurrentWeather(ctx, location string) (Weather, error) } and a Weather struct (temp, condition, location). Add a StubProvider that returns a sentinel ErrNotConfigured (or a "погода не настроена" message).
  2. Config: add Weather *WeatherConfig to VoiceConfig (fields: Provider, APIKey, DefaultLocation — all omitempty). No API key in the repo.
  3. Optionally add an Open-Meteo provider struct that is not called at night (no key needed) — but if you write it, do NOT make a network call in tests; test against a mocked HTTP round-tripper only. If that's too fiddly, ship just the interface + stub and leave a // TODO: open-meteo provider — that's fine.
  4. Add a real Open-Meteo provider (keyless — no API key needed). Endpoint: https://api.open-meteo.com/v1/forecast?latitude=..&longitude=..&current_weather=true. Geocode via https://geocoding-api.open-meteo.com/v1/search?name=<location>. Keep the *http.Client injectable (a struct field) so tests use a mocked round-tripper — no real network call in any test. Config selects provider by voice.weather.provider ("open-meteo" | "" → stub).
  5. Wire it into voice.go. In the IntentQuery handler, detect a weather question (keywords погода/градус/температура, or a query_weather sub-path) → call the configured provider with DefaultLocation or a parsed location → format an RU reply. Unconfigured → the stub's «погода не настроена» message. Seed models/seeds/query.txt with «какая погода», «какая погода в москве». Bound the provider call with a context timeout (~5s) so a slow API can't hang the voice turn.

Done when: stub + mocked-Open-Meteo tests pass (round-trip against a fake transport, unconfigured → stub message, location parsing), gate green. Split into two commits if helpful: provider+interface, then voice wiring.


Task 6 — Dialogue state scaffold (pure data structures)

Goal: a session/context data layer for future multi-turn. No LLM, no wiring into the live path unless trivial and tested.

Files: new internal/dialogue/ package + tests only.

Do:

  1. internal/dialogue/session.go: a Session holding last-turn intent + slots, a timestamp, and a TTL (default ~2min, configurable via a field). A SessionStore (in-memory map keyed by session id) with Get, Put, and TTL-based expiry.
  2. A pure InheritSlots(prev, cur Slots) Slots helper: carry forward slots the current turn is missing (e.g. previous had a location, current didn't → use previous). Copy the Slots shape from internal/router/intent.go.
  3. Tests: context carry-over, slot inheritance, session expiry, missing prior session. This is the whole task — it's a tested library, not a feature.

Done when: tests pass, gate green. Then (only if the library is solid and gate is green) wire a minimal read seam into voice.go: on a follow-up-shaped utterance, look up the prior session's slots and fill the current turn's missing slots via InheritSlots before routing. Keep the session store's lifetime owned by the handler struct. If wiring gets fiddly or risks the live path, ship the tested library and mark the wiring BLOCKED — the library is the required part. Separate commits: library, then wiring.


Task 7 — Long-term memory vector-store interface (pure)

Goal: an interface + in-memory implementation for a future vector backend.

Files: new internal/memory/store.go + tests only.

Do:

  1. type Store interface { Insert(ctx, id string, vec []float32, meta map[string]string) error; Search(ctx, vec []float32, topK int) ([]Result, error) }. Result = id, score, meta.
  2. An InMemoryStore implementing it with cosine similarity (copy the cosine function idea from internal/router/classifier.go — you may factor a shared helper, but simplest is to reimplement locally; don't refactor the router).
  3. Tests: insert→search round-trip, cosine ordering (nearest first), topK truncation, metadata filtering if you add it. In-memory only.

Done when: tests pass, gate green (library commit). Then wire the embedding pipeline: in the IntentNote handler in voice.go, after WriteNote, also Insert the note's embedding + metadata (id, source, ts) into the memory Store. Use the same embedder the classifier uses (already in scope as h.embedder). Make the memory Store a field on the handler, defaulting to InMemoryStore so nothing external is required. Wrap the Insert in its own error branch — a memory Insert failure must not fail the note write (log and continue). Separate commit for the wiring.


Phase Final — Verification pass (always do this last)

  • gofmt -l . prints nothing
  • go build ./... succeeds
  • go vet ./... clean
  • go test ./... green
  • docker compose build succeeds (all daemons compile) — if docker is unavailable in the environment, note it and rely on go build ./....
  • git log --oneline master..HEAD — confirm one commit per completed task, each message names its task, no "wip"/debug commits.
  • Grep for accidents: grep -rn "destructive.*false" deploy/mavend.json and eyeball every hit; grep -rniE "TODO|FIXME|panic\(|fmt.Println" cmd internal — no stray debug prints, no new panics in live paths.
  • Update the Status column of each task in this file (Done / BLOCKED:reason / Skipped) so the human can see what happened at a glance.

Status board (fill this in as you go)

# Task Commit Status
1 Embedder config validation + docs b778f0b Done
2 Seed safe tool allowlist 3b8fb69 Done
3 Calendar querying cf066bd Done
4 General-knowledge phraser routing 428af3f Done
5 Weather skeleton (pure) e030466 Done
6 Dialogue scaffold (pure) 79eb43e Done
7 Memory vector interface (pure) 880715f Done
F Final verification pass d52f60c Done — all gates green

DEFERRED — needs a human, DO NOT ATTEMPT unsupervised

These were in the original plan. They require hardware or protocol decisions and will burn the night if attempted blind. Left here only so you don't rediscover them and think they were forgotten.

  • Always-on listening / wake word (internal/wake/, cmd/mavmic/): needs a hardware decision (USB mic vs Pi vs smart speaker) and a Porcupine license/key. Human input required.
  • Streaming STT over WebSocket + barge-in (internal/voice receive path): changes the wire protocol (PROTOCOL.md) and the STT worker contract. Too invasive to do safely unsupervised; risks breaking the working full-clip path.
  • Streaming TTS: blocked on Piper. Out of scope.

If you finish Tasks 17 with time to spare, do NOT start these. Instead: improve test coverage on what you built, expand the seed files, and improve docs.