348 lines
17 KiB
Markdown
348 lines
17 KiB
Markdown
# 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: <reason>"), 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 144–159), 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=..¤t_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 1–7 with time to spare, do NOT start these. Instead: improve
|
||
test coverage on what you built, expand the seed files, and improve docs.
|