chore: initial commit — spec + implementation plan for talk 2.0

This commit is contained in:
m3ta-chiron committed 2026-10-18 15:05:24 +02:00
commit 1d32f4dc71
2 files changed
+266

No files matched your search

+164
View File
@@ -0,0 +1,164 @@
# Plan: talk 2.0 — V1 Implementationsplan
Plan: .scratch/talk-2-0/plan.md
Spec: .scratch/talk-2-0/spec.md (bindende Autorität — Konflikte gehen zu ihren Lasten)
Branch: feat/v1 (ab main; Repo wird in Task 01 initialisiert)
## Global Constraints
(aus der Spec, bindend für alle Tasks — bei Konflikt gewinnt die Spec)
- **Sprache/Laufzeit:** Go; statische Binaries für Windows/macOS/Linux × amd64/arm64; keine Runtime-Abhängigkeiten (kein ffmpeg, kein mpv, kein Python).
- **Binaries:** `talk` (CLI) + `talkd` (Daemon). Modulname: `code.m3ta.dev/m3tam3re/talk`.
- **CLI-Surface (exakt):** Default-Befehl `talk "Text"` = non-blocking; Flags `--wait`, `--now`, `--priority`, `--voice`, `--lang`, `--provider`. Subcommands: `say`, `wrap`, `mute`, `unmute`, `status`, `cancel`, `voices`, `test`, `doctor`, `setup`. `--help` auf jedem Subcommand.
- **Provider-Kette (Fallback-Reihenfolge):** ElevenLabs → OpenAI TTS → Edge-TTS (frei, kein Key) → OS built-in (macOS `say`, Windows SAPI, Linux espeak/speech-dispatcher). Überschreibbar per Config/Flag. Keys Env-first: `TALK_ELEVENLABS_KEY`, `TALK_OPENAI_KEY`. Provider-Endpunkte konfigurierbar (Fake-bar für Tests).
- **Config:** optionale `config.toml` im platform-nativen Verzeichnis: Linux `~/.config/talk`, macOS `~/Library/Application Support/talk`, Windows `%APPDATA%\talk`. Env schlägt Datei. Voice-Mapping `voice.<provider>.<lang> = id`.
- **Audio:** Playback embedded via oto; Audio-Sink wählbar inkl. `null` (CI/Server). Notifications best effort via beeep.
- **Daemon:** talkd besitzt Queue (Serialisierung, Prioritäten, Rate-Limit Default 1 Ansage/30 s, prioritätsbewusstes Verwerfen), persistenten Mute-State (Datei im Config-Dir), Ruhezeiten `quiet_hours = "18:00-08:00"` (lokale Zeit; still verwerfen oder toast-only), HTTP-API `POST /speak` auf 127.0.0.1 mit konfigurierbarem CORS, MCP-Endpoint (Streamable HTTP) auf localhost.
- **Degraded Mode:** ohne talkd: Lock-File + sequenzielles Abspielen; nichts bricht still.
- **TUI (Bubble Tea):** interaktiv nur `setup` (Wizard), `status` (Live-Dashboard), `voices` (Picker mit Preview); alle anderen Befehle plain. Kein TTY → alle Befehle plain. Agenten-Pfade (Args/stdin) starten nie eine TUI.
- **Etikette (nur Skill-Doku, kein Daemon-Session-State):** Opt-in; Ansageinhalt = Status/nächster Schritt/Freigabe-Bedarf, nie Langantwort.
- **Testing:** Primäre Seam = Prozessgrenze talkd+CLI als Black Box mit injizierbaren Fakes über Config (Provider-Endpunkt → Fake-HTTP-Server mit Audio-Bytes; `audio.sink = "null"`). Reine Unit-Seams: langdetect, Config-Auflösung. Echt-Audio und OS-Toasts ungetestet (doctor-Rendering manuell). Tests testen nur extern beobachtbares Verhalten.
- **CI:** Gitea Actions — `.gitea/workflows/test.yml` läuft `go vet` + `go test ./...` bei jedem Push. `go build ./...` muss auf dem Dev-Host (Linux) grün sein; Cross-Compile via `GOOS`/`GOARCH` mindestens einmal in Task 11 verifiziert.
- **Lizenz:** MIT (LICENSE-Datei ab Task 01).
- **Doku-Sprache:** Deutsch (README, Skill).
- **Commits:** Conventional Commits; keine Pushes; keine Änderungen außerhalb des Repos (Ausnahme Task 12, siehe dort).
## Task 01: Repo-Skeleton & CI-Backbone
Keine Blocker. Liefert: initialisierbares, grünes Go-Repo.
- `git` bereits initialisiert (main); dieser Task entsteht auf Branch `feat/v1` (besteht bereits).
- Go-Modul `code.m3ta.dev/m3tam3re/talk` (go.mod, aktuelle stabile Go-Version des Dev-Hosts).
- Verzeichnisgerüst: `cmd/talk/`, `cmd/talkd/`, `internal/{provider,queue,audio,notify,config,api,langdetect}/` (jeweils mit placehalterfreiem Miniinhalt oder leer + doc.go — kein toter Code).
- `cmd/talk/main.go`: Subcommand-Gerüst (cobra oder stdlib flag — Entscheidung: **stdlib `flag` + eigene Subcommand-Dispatch**, keine externen CLI-Abhängigkeiten in T01) mit `--version` (zeigt `dev` + Commit via ldflags-Variablen, default `dev`) und allen Subcommands als `not implemented`-Stubs mit korrektem `--help`.
- `cmd/talkd/main.go`: startet, loggt eine Zeile, exit 0 (Platzhalter).
- `.gitea/workflows/test.yml`: `go vet ./...`, `go test ./...`, `go build ./...` bei push/PR.
- `.gitignore` (Go-Binaries, `dist/`), `LICENSE` (MIT, Copyright 2026 m3tam3re), `README.md`-Gerüst (Projektname, Ein-Satz-Zweck, Status WIP, Hinweis auf Spec).
- Mind. ein sinnvoller Test, der die CI-Struktur verifiziert (z. B. Version-String-Test).
- Abnahme: `go vet && go test && go build` grün; Workflow-Datei valider YAML; README/LICENSE vorhanden; Commit(s) conventional.
## Task 02: Erste Ansage end-to-end (OS-TTS)
Blockiert durch: 01. Liefert: `talk "Hallo Welt"` spricht über OS-TTS.
- `internal/langdetect`: reine Funktion `Detect(text string) string` → `"de"` | `"en"` (Default `"de"`); Heuristik: Common-Word-Listen + Umlaute/ß; tischgesteuerte Tests (de/en/gemischt/leer).
- `internal/provider`: Interface `Provider` mit `Name() string`, `Available(ctx) bool`, `Synthesize(ctx, text string, lang string) ([]byte, error)` (Rückgabe: dekodierbare Audio-Bytes, Format von der Implementierung dokumentiert; osbuiltin liefert WAV/PCM).
- Implementierung `osbuiltin`: Linux über `espeak`/`speech-dispatcher`-Fallback (externer Prozess — OK, es ist der Notnagel-Provider; wenn Binary fehlt: `Available() == false`), macOS über `say` (Ausgabe in temporäre WAV-Datei), Windows über SAPI (PowerShell `System.Speech`). Fehlende Binary/Runtime → klarer Fehler, kein Panik.
- `internal/audio`: `Sink`-Interface (`Play(ctx, data []byte) error`, `Name() string`); Implementierungen `oto` (embedded Playback, PCM/WAV) und `null` (verwirft, loggt). Auswahl per Config `audio.sink = "oto"|"null"` (Default `oto`).
- `internal/config`: minimale Auflösung: Defaults + TOML-Datei (falls vorhanden) + Env-Overrides (`TALK_AUDIO_SINK`); Platform-Pfad-Auflösung (`~/.config/talk` | `~/Library/Application Support/talk` | `%APPDATA%\talk`, respektive `XDG_CONFIG_HOME`/Env); tischgesteuerte Unit-Tests für Merge + Pfadlogik (Pfade injizierbar).
- `cmd/talk`: `say`-Pfad implementiert: Text (Arg oder stdin) → langdetect → Providerauswahl (hier: osbuiltin, Kette kommt in Task 03; Struktur so, dass `--provider`-Flag schon existiert und `osbuiltin` default ist) → Synthesize → Sink.Play. Blocking (Daemon kommt in Task 04).
- Abnahme: `talk "Hallo Welt"` erzeugt hörbare Ausgabe auf dem Dev-Host (Linux; espeak vorhanden oder als Dev-Abhängigkeit dokumentiert); `TALK_AUDIO_SINK=null` läuft ohne Ton durch; `go test ./...` grün (langdetect + config + provider-mit-fake-proc wo sinnvoll).
## Task 03: Provider-Kette mit Cloud-Anbietern
Blockiert durch: 02. Liefert: Vier-Stufen-Fallback mit Keys + Fake-barkeit.
- Provider-Implementierungen (je `internal/provider/<name>`):
- `elevenlabs`: POST `<endpoint>/v1/text-to-speech/<voiceID>`, Header `xi-api-key`, Body `{text, model_id: "eleven_multilingual_v2", voice_settings:{stability:0.5,similarity_boost:0.75}}`, Antwort = Audio-Bytes (mp3). Key: `TALK_ELEVENLABS_KEY`; Default-Voice-ID `KXxZd16DiBqt82nbarJx`. Endpunkt konfigurierbar (Default `https://api.elevenlabs.io`).
- `openai`: POST `<endpoint>/v1/audio/speech`, Bearer-Key `TALK_OPENAI_KEY`, Body `{model:"gpt-4o-mini-tts"|"tts-1", voice, input, response_format:"mp3"}` — Entscheidungsnotiz: Modell-String konfigurierbar, Default `tts-1`. Endpunkt Default `https://api.openai.com`.
- `edge`: Microsoft Edge-TTS-Endpunkt (kostenlos, kein Key) — WebSocket/REST nach aktuellem bekannten Protokoll (edge-tts-kompatibel, Stimmen z. B. `de-DE-KatjaNeural`/`en-US-AriaNeural`); Endpunkt konfigurierbar; bei Protokoll-Änderung: klarer Fehler.
- `osbuiltin`: existiert (Task 02).
- Kette `internal/provider/chain.go`: Resolver liefert geordnete Kette laut Config (Default-Reihenfolge oben); `--provider`-Flag und `provider = "…"`-Config pinnen; `Available()` prüft Key/Endpunkt; Synthesize fällt bei Fehler (HTTP != 2xx, Timeout, Netz) auf den nächsten durch; letzter Fehler wird geloggt/gemeldet, nie still.
- Voice-Mapping: `voice.<provider>.<lang>` aus config.toml; Defaults je Provider+Sprache hinterlegt (neutral, dokumentiert).
- Tests: jeder Cloud-Provider gegen `httptest`-Fake (200 mit Bytes; 500 → Fehler; 401 → Fehler) **außer edge** (WebSocket-Protokoll — edge wird über Interface + Fehlerpfad-Mock und eine als `testing.Short` überspringbare Integration-Testmarkierung behandelt; Dokumentationspflicht im Report, welche Teile fake-bar getestet sind); Kettentests: erster 500 → zweiter getroffen → Bytes.
- Abnahme: `talk "Test" --provider=elevenlabs` mit echtem Key funktioniert am Dev-Host ( einmalig manuell, im Report dokumentiert); ohne jeden Key wählt die Kette edge bzw. osbuiltin; `go test ./...` grün.
## Task 04: talkd-Daemon + Queue + Mute
Blockiert durch: 02, 03. Liefert: Daemon-betriebene, serialisierte Ansagen mit persistentem Mute.
- `cmd/talkd`: Daemon-Prozess; HTTP-Server auf `127.0.0.1:<port>` (Port Default `7800`, konfigurierbar `TALK_PORT`/config `server.port`); Endpunkte:
- `POST /speak` — JSON `{text, priority?, provider?, voice?, lang?, now?}` → enqueued, Antwort `202 {id}`; bei Mute: `202 {muted:true}` (verworfen, dokumentiertes Verhalten).
- `GET /status` — JSON: Queue-Länge, aktuelles Item, Mute-State, aktiver Provider, Version.
- `POST /cancel` — bricht aktive Ansage ab + leert Queue, Antwort `200 {cancelled:n}`.
- `POST /mute`, `POST /unmute` — toggelt persistenten Mute-State (Datei `mute` im Config-Dir).
- `internal/queue`: serialisierte Abarbeitung (eine Ansage gleichzeitig), Priorität (high > normal > low; gleiches Level: FIFO), `now`-Items springen vor; IDs (z. B. fortlaufend/zufällig kurz); konfigurierbares Rate-Limit existiert als Struktur, Limit-Wiring folgt Task 05 (Default unendlich in T04-Tests, dokumentiert).
- CLI-Anbindung: `talk say` (und Default) erkennt Daemon via `GET /status`; läuft er: enqueue + sofort zurück (non-blocking Default); `--wait` pollt bis abgearbeitet; `--now` setzt `now:true`; `mute`/`unmute`/`status`/`cancel` werden Daemon-Aufrufe mit plain Output.
- Degraded Mode: Daemon antwortet nicht → Lock-File `talk.lock` im Config-Dir, sequenzielles lokale Abspielen (best-effort; Kollision = warten), mute wird respektiert.
- Tests an der **Prozessgrenze** (primäre Seam): talkd mit Fake-Provider-Endpunkt + `audio.sink=null` starten; per CLI/HTTP enqueue; assert: Serialisierung (2 schnell nacheinander → nacheinander abgespielt, an Fake+Sink beobachtbar), Priorität, Mute-Persistenz über Daemon-Restart, cancel, non-blocking-Verhalten (say kehrt vor Abspielende zurück), Degraded Mode (Daemon tot → Lock-File-Pfad).
- Abnahme: alle Prozessgrenzen-Tests grün; `talkd &` + 3 parallele `talk`-Aufrufe → drei Ansagen nacheinander; `talk mute` → Daemon-Restart → weiter stumm.
## Task 05: Rate-Limit + Ruhezeiten
Blockiert durch: 04. Liefert: Daemon-Gesetz gegen Spam.
- Rate-Limit in Queue: Default **1 Ansage / 30 s** (konfigurierbar `queue.rate_limit = "30s"`); Tokens/Zeitfenster-Logik; `now:true` umgeht Warteschlangen-Wartezeit nicht das Rate-Limit; bei Überschreitung: Items mit `priority=low` werden **verworfen**, `normal` wartet, `high` darf konfigurierbar verstoßen (`queue.allow_high_burst = true|false`, Default true).
- `quiet_hours`: Config `quiet_hours = "18:00-08:00"` (lokale Zeit, 24h, über Mitternacht); Verhalten configbar: `quiet_mode = "discard"|"toast"` (Default `discard`); Mute (manuell) hat immer Vorrang; `now:true` bricht auch quiet hours (bewusster Ausbruch, dokumentiert).
- Tests an der Prozessgrenze: kurzes Rate-Limit (z. B. 1/s) konfigurieren; 5 enqueues → 1 abgespielt, low verworfen, normal geparkt; quiet_hours-Fenster künstlich um die aktuelle Zeit legen → discard/toast-Verhalten.
- Abnahme: Tests grün; `talk status` zeigt Rate-Limit- und Quiet-Hours-State.
## Task 06: CORS-Tür + MCP-Endpoint
Blockiert durch: 04. Liefert: Browser- und MCP-Anbindung auf localhost.
- CORS: `POST /speak` (und nur dieser) beantwortet `OPTIONS`-Preflight und setzt `Access-Control-Allow-Origin` aus Config-Liste `server.allowed_origins = ["https://chat.az-gruppe.com"]` (Wildcard `*` erlaubt aber nicht Default; leere Liste = kein CORS-Header). Tests: Preflight 204 + Header; nicht gelistete Origin → kein ACAO-Header.
- MCP: Streamable-HTTP-Endpoint `POST /mcp` (und ggf. `GET /mcp` für Stream) auf localhost; Implementierung des MCP-Protokolls (initialize, tools/list, tools/call) ohne schwergewichtige SDK — Decision: **eigene schlanke Implementierung des JSON-RPC-Subsets** (initialize, notifications/initialized, tools/list, tools/call), kein externer MCP-SDK-Dependency-Zwang; falls eine begründete leichte SDK existiert, im Report begründen.
- Ein Tool `speak`: Input-Schema `{text: string, priority?, provider?, lang?, now?}` → verhält sich wie POST /speak; Response = MCP-Tool-Result (Text: "queued: <id>" o. Ä.).
- `GET /status` um Felder `mcp: true, cors: [origins]` ergänzt (Compat).
- Tests: MCP-Handshake + tools/call an der Prozessgrenze (HTTP-Payloads literal getestet); CORS-Preflights.
- Abnahme: curl-Preflight von gelisteter Origin ok; ein MCP-fähiger Client (z. B. manuelle JSON-RPC-Curl-Sequenz im Test/Report) sieht `speak` und enqueued erfolgreich.
## Task 07: `talk wrap`
Blockiert durch: 04. Liefert: Befehlsummwicklung mit Ansage.
- `talk wrap -- <befehl> [args…]`: führt Befehl aus (stdin durchreichen, stdout/stderr durchreichen), misst Dauer, Ansage nach Ende: Erfolg: „<label> erfolgreich. Dauer: X." / Fehler: „<label> fehlgeschlagen, Code N." — Label = Config `wrap.label` oder der Basisname des Befehls; Ansage läuft über den Daemon-Pfad (enqueue, non-blocking; `--wait`-Flag respektiert).
- Exit-Code des gewrappten Befehls wird durchgereicht (talk-Fehler ≠ Befehls-Fehler: wrap exited mit Befehls-Code; wenn die Ansage selbst scheitert: Warning auf stderr, Befehls-Code bleibt).
- Dauer-Formatierung: menschlich („3 Minuten 42 Sekunden", lokalisiert de/en nach langdetect des Labels).
- Tests: Unit für Formatierung + Prozessgrenze: `talk wrap -- sleep 1` mit Fake/Sink null → enqueue-Call mit Erfolgs-Ansage verifiziert; fehlschlagender Befehl → Code 2 durchgereicht + Fehler-Ansage.
- Abnahme: `talk wrap -- sleep 2` annonciert Erfolg + Dauer; Exit-Codes korrekt.
## Task 08: `doctor` / `test` / `voices` (plain)
Blockiert durch: 03, 04. Liefert: Selbstdiagnose und plain Oberflächen.
- `talk doctor`: prüft und berichtet zeilenweise mit ✔/✘/⚠: Daemon erreichbar (Port/Status), Audio-Sink (oto initialisierbar; null → Hinweis), Provider-Verfügbarkeit je Stufe (Key vorhanden? Endpunkt erreichbar — leichtgewichtiger Probe-Call konfigurierbar `doctor.probe = true|false`, Default false → nur Key/Config-Checks, keine Netz-Calls), Notification-Daemon (beeep available), Config-Pfad + effektiv geladene Werte (Keys maskiert), Mute/Rate-Limit/quiet_hours-State via Daemon. Exit-Code 0 wenn kritikalfrei, 1 wenn ✘ (Daemon tot zählt nicht als ✘, sondern ⚠ mit Hinweis Degraded Mode). Machine-lesbar via `--json`.
- `talk test`: spricht feste Testphrase (de/en je `--lang`), zeigt verwendeten Provider + gemessene Latenz; ohne Daemon → direkter Pfad.
- `talk voices`: plain Tabelle `PROVIDER LANG VOICE-ID NAME DEFAULT` aus der vereinheitlichten Voice-Registry (statische Defaults je Provider + config-overrides); `--provider`/`--lang`-Filter; `--json`.
- Tests: doctor gegen laufenden/keinen Daemon an der Prozessgrenze (mit sink null + fake provider); voices-Output-Format Unit.
- Abnahme: bei kaputtem Setup (falscher Port, Key weg) nennt doctor die Punkte explizit; `talk test` funktioniert einmal end-to-end am Dev-Host.
## Task 09: Bubble-Tea-TUI: `setup` / `status` / `voices`
Blockiert durch: 03, 08. Liefert: geführtes Onboarding, Live-Dashboard, Voice-Picker.
- Dependency: `github.com/charmbracelet/bubbletea` (+ minimal nötige charm-libs wie lipgloss/bubbles — begründete Auswahl im Report).
- TTY-Erkennung: kein TTY (Pipe/CI/Agent) → plain-Fallback-Output derselben Info (nie Stacktrace, nie interaktive Schleife).
- `talk setup` (Wizard): Schritt 1 Daemon/Version prüfen (Hinweis wenn tot, fortfahrbar) → Schritt 2 Provider-Keys erkennen (env) + optional Key-Eingabe (maskiert) + Probe → Schritt 3 Stimme je Provider/Sprache probehören (Auswahl → `test`-Absage mit dieser Stimme) → Schritt 4 config.toml schreiben (existierende Datei: zusammenführen, Backup `.bak` anlegen) → Abschlussbericht. Abbruch jederzeit mit q/Ctrl-C, nichts halbfertig schreiben.
- `talk status` (interaktiv): Live-Ansicht Queue (Items mit ID/Priorität/Alter), aktuelles Item mit Fortschritt, Mute/quiet hours/rate-limit, Provider-Kette mit Verfügbarkeit; Aktualisierung via Poll; `m` = mute-toggle, `c` = cancel, `q` = quit — Tasten in Footer.
- `talk voices` (interaktiv): Liste filterbar (`/`), Enter = Probe abspielen (über Daemon), `s` = als Default in config setzen (mit Bestätigung).
- Plain-Pfade von 08 bleiben unangetastet (TTY-Weiche in derselben Command-Funktion).
- Tests: Modell-Unit-Tests (Wizard-State-Machine: Schritte, Abbruch, Merge-Entscheidung; Status-Rendering-Funktion; Voices-Filter) — TUI-Renderlogik als reine Funktionen halten; Prozessgrenze: `talk status < /dev/null` → plain, exit 0.
- Abnahme: `talk setup` durchläuft am Dev-Host komplett (mit Fake-Provider-Endpunkt demonstriert im Report/Tests); keine TTY-Pfade brechen.
## Task 10: Skill + AGENTS.md-Snippet + README
Blockiert durch: 08. Liefert: Etikette-Doku als konsumierbares Skill-Artefakt.
- `skills/talk/SKILL.md` (Frontmatter analog etablierter Skills: name, description mit Triggern) mit: When to Use (User-Trigger ODER Standing-Order in AGENTS.md — **striktes Opt-in**), When NOT to Use, Usage (CLI-Beispiele: say/wrap/status/mute; stdin-Pipe; --wait/--now/--priority), Etikette (Ansageinhalt = Status/nächster Schritt/was der Nutzer tun soll; max 1–2 Sätze; niemals Langantwort; session-sticky bis Widerruf; Mute/quiet hours respektieren — Daemon erzwingt es ohnehin), Degraded-Verhalten, Pitfalls (Key-Env, kein Ton → doctor).
- `skills/talk/AGENTS-snippet.md`: 3-Zeilen-Block für Projekt-AGENTS.md (Standing-Order-Beispiel).
- `README.md` ausbauen: Quickstart (Installation Release-Artefakt/Nix, `talk setup`, erster Test), CLI-Referenz (alle Befehle/Flags), Config-Referenz (vollständiges config.toml-Beispiel mit allen Keys aus den Tasks 02–09), Daemon (talkd, Autostart-Hinweise je OS), Provider-Tabellen (Keys, Kosten, Defaults), Fleet-Hinweis (Env-Pattern TALK_*, interne Spiegelung, doctor als Diagnose), Etikette-Sektion, FAQ (kein Ton? Browser/LibreChat-Später, MCP-Anbindung-Beispiel).
- Abnahme: README deckt jede CLI-Surface aus den Global Constraints ab; Skill enthält die Opt-in-Policy wörtlich-diszipliniert; keine neuen Code-Änderungen nötig (Docs-only-Task — Tests: `go test ./...` bleibt grün, Markdown-Links intern valide).
## Task 11: Release-Pipeline + flake.nix
Blockiert durch: 09, 10. Liefert: getaggte Multi-Platform-Artefakte + Nix-Bezug.
- `.goreleaser.yaml`: Builds `talk` + `talkd` (zwei Binaries), Targets linux/darwin/windows × amd64/arm64 (Windows-Suffix `.exe`), Archiv je OS; **Asset-Namensvertrag: `talk_<os>_<arch>`** (+ `.zip`/`.tar.gz` je Konvention, Checksums-Datei `checksums.txt`), ldflags: Version-Injection (`-X main.version={{.Version}}` bzw. passend zur T01-Struktur).
- `.gitea/workflows/release.yml`: bei Tag `v*`: test → goreleaser → Upload zu Gitea-Releases.
- Cross-Compile-Verifikation: einmalig lokal `GOOS/GOARCH`-Matrix bauen (im Report dokumentiert); CGO-Grundsatz:oto braucht gopherus? → **Verpflichtung: oto-Abhängigkeit muss cross-compilierbar sein (reiner Go-Audiopfad oder build-tag-gelöste Plattformpfade); falls CGO nötig würde: Stattdessen plattformnative Fallback-Player als build-tag-Varianten** — im Report die gefundene Lösung dokumentieren; das ist der risikoreichste Punkt dieses Tasks.
- `flake.nix`: Package `talk` + `talkd` (buildGoModule), `apps`/`packages`-Outputs, devShell; ersetzt langfristig m3ta-nixpkgs talk/crunch (Hinweis-Kommentar in flake.nix).
- Abnahme: `nix build .#talk` funktioniert am Dev-Host; `goreleaser build --snapshot --clean` produziert alle Targets ohne Fehler; Workflow-YAML valide.
## Task 12: Legacy-Migration (AGENTS-Repo)
Blockiert durch: 10, 11. **Achtung: greift in ein ANDERES Repo (`~/p/AI/AGENTS`) — ausschließlich die zwei Skill-Dateien `skills/voice-notify/SKILL.md` und `skills/crunch-jobs/SKILL.md` als Redirect-Stubs umschreiben; kein Commit dort ohne die im Brief dokumentierte Reihenfolge; kein Push.**
- Beide SKILL.md-Stubs: Frontmatter behalten (description kürzt auf „DEPRECATED: replaced by talk 2.0 …"), Body: kurzer Umstiegs-Text (neues Repo, neuer Skill-Name, CLI-Beispiel, Link zur talk-README), keine alten Anleitungen mehr.
- `docs/migration-notes.md` im talk-Repo: Checkliste Restarbeiten (PRs #24/#7 in m3ta-nixpkgs als superseded schließen — Nutzer-Aktion; m3ta-nixpkgs talk/crunch deprecaten — Nutzer-Aktion mit Vorlage-Text; Fleet-Ausrollung plant).
- Abnahme: Stubs existieren und sind eindeutig; migration-notes listet alle Nutzer-Aktionen; im AGENTS-Repo liegt ein unpushed Commit auf einem neuen Branch `chore/talk2-migration` (kein main-Commit!).
## Task 13: Altlast-Entsorgung talk.sh (Nutzer-gated)
Blockiert durch: KEINE Tasks, aber durch **Nutzer-Aktion: ElevenLabs-Key-Rotation im Dashboard**. Ohne bestätigte Rotation NICHT ausführen (destruktiv + sicherheitsrelevant). Wenn der Nutzer Rotation bestätigt hat: `~/talk.sh` löschen (rm), in migration-notes abhaken, Ledger-Eintrag. Dieser Task bleibt parked bis zur Bestätigung.
+102
View File
@@ -0,0 +1,102 @@
# Spec: talk 2.0 — Cross-Platform Voice Notifications für KI-Agenten
Status: ready-for-agent
Created: 2026-10-18
Source: Grilling-Session 2026-10-18 (shared understanding, alle Äste besucht)
---
## Problem Statement
KI-Agenten (Coding-Agenten wie opencode/Claude Code, Firmen-Chats, Hintergrund-Jobs) können mich heute nur visuell erreichen — Terminal-Output, Toast, push notification. Wenn ich weg vom Bildschirm bin (Kaffee, anderes Fenster, Meeting am zweiten Monitor), verpasse ich, dass ein Build fertig ist, ein Agent auf meine Freigabe wartet oder ein Job gecrasht ist.
Der bestehende talk/crunch-Stack löst das prinzipiell, ist aber unbrauchbar als Basis: bash + mpv + notify-send + agenix = Linux-Desktop-only, ElevenLabs-only (Key pro Host, in der Praxis nur auf einem bestätigt funktionierend), kein natives Tool-Interface für Agenten (nur Shell-Umwege), keine Queue (Audio überlappt), flüchtiger Mute-Toggle, und null Geschichte auf dem Windows-Fleet der Firma.
## Solution
**talk 2.0**: Ein cross-platform Werkzeug (Windows/macOS/Linux, amd64+arm64) in Go als statische Binaries — `talk` (CLI mit Bubble-Tea-TUI) und `talkd` (Daemon mit Queue). Jeder KI-Agent, der ein Programm ausführen kann, kann damit sprechen: `talk "Build fertig"`. Lokale MCP-Clients bekommen zusätzlich einen nativen MCP-Endpoint auf localhost. Die TTS-Qualität skaliert mit verfügbaren Keys über eine Vier-Stufen-Fallback-Kette bis hinunter zu kostenlosen/offline-Stimmen — Stimme funktioniert also überall ab Minute eins. Sprechen ist striktes Opt-in durch den Nutzer; der Daemon erzwingt Rate-Limit, Mute und Ruhezeiten als Backstop unter allem Prompt-Verhalten.
Verteilung: Gitea-Releases nach agent-lib-Muster (Asset-Schema `talk_<os>_<arch>` + sha256); Firma: interne Spiegelung + Fleet-Role (Ansible, Program Files + PATH); privat: Nix-Flake. Skill lebt im Repo und rollt in der Firma über az-agent-defaults/agent-lib aus.
## User Stories
1. Als **Nutzer** will ich `talk "Text"` aufrufen können, damit ich sofort weiß, wie die Schnittstelle funktioniert, ohne Doku zu lesen.
2. Als **Nutzer** will ich, dass der Aufruf non-blocking zurückkehrt, damit mein Skript/Agent nicht auf das Audio wartet.
3. Als **Skript-Autor** will ich `talk "Text" --wait`, damit Cron-Jobs erst nach der Ansage weiterlaufen bzw. enden.
4. Als **Nutzer** will ich `talk wrap -- <befehl>` nutzen können, damit mir Dauer + Erfolg/Misserfolg eines beliebigen Befehls angesagt werden.
5. Als **KI-Agent** will ich das Programm mit Argument oder via stdin füttern können, damit ich es aus jeder Umgebung heraus aufrufen kann.
6. Als **KI-Agent (MCP-fähig)** will ich einen lokalen MCP-Endpoint mit einem speak-Tool nutzen können, damit ich Voice nativ als Tool habe statt Shell-Umwege.
7. Als **Browser-KI-Chat (später)** will ich `POST /speak` auf localhost mit CORS-Erlaubnis rufen können, damit eine Web-UI (z. B. LibreChat) mich ansprechen kann, ohne dass talk dafür geändert werden muss.
8. Als **Nutzer mit ElevenLabs-Key** will ich die Premium-Stimme bekommen, damit Ansagen so klingen wie gewohnt.
9. Als **Nutzer mit nur einem OpenAI-Key** will ich OpenAI-TTS als Provider nutzen können, damit ich keinen zweiten Account brauche.
10. Als **Nutzer ohne jeden Key** will ich automatisch Edge-TTS (kostenlos) bekommen, damit Stimme out-of-the-box funktioniert.
11. Als **Offline-Nutzer** will ich auf OS-eingebaute TTS-Stimmen (say/SAPI/espeak) zurückfallen, damit auch ohne Netz/Key gesprochen wird.
12. Als **Nutzer** will ich, dass die Provider-Kette automatisch durchfällt (Key ungültig, Kontingent leer, Netz weg), damit nie stumm bleibt, wenn nicht sein muss.
13. Als **Nutzer** will ich die Sprache des Texts automatisch erkannt bekommen (de/en), damit englische Agent-Antworten nicht mit deutscher Stimme verhackstückt werden.
14. Als **Nutzer** will ich pro Provider und Sprache eine Stimme in der config.toml setzen, damit meine gewohnte Stimme überall gleich klingt.
15. Als **Nutzer** will ich `talk setup` als geführten Wizard (Stimmen probehören, Key testen, Config schreiben), damit ich ohne Terminal-Erfahrung in 2 Minuten eingerichtet bin.
16. Als **Nutzer** will ich `talk status` als Live-Dashboard (Queue, Mute, aktiver Provider, Version), damit ich immer weiß, was das Tool gerade tut.
17. Als **Nutzer** will ich `talk voices` als interaktive Liste mit Vorhör-Preview, damit ich Stimmen ohne IDs auswendig zu lernen vergleichen kann.
18. Als **Nutzer** will ich `talk mute` / `talk unmute` als persistenten Schalter, damit alle Agenten und Skripte still sind, bis ich es erlaube.
19. Als **Nutzer** will ich konfigurierbare Ruhezeiten (`quiet_hours = "18:00-08:00"`), damit 23:47-Uhr-Cron-Jobs mich nicht wecken.
20. Als **Nutzer** will ich `talk cancel`, damit eine laufende oder geplante Ansage sofort stoppt.
21. Als **Nutzer** will ich `--priority`, damit Dringendes die Queue vorn überspringt und Gedrängel bei Rate-Limit verworfen werden darf.
22. Als **Nutzer** will ich, dass sich Audio-Nachrichten nie überlappen (Queue serialisiert), damit bei drei gleichzeitigen Agenten alles verständlich bleibt.
23. Als **Nutzer** will ich ein Rate-Limit als Daemon-Gesetz (Standard ~1 Ansage/30 s), damit kein Agent-Spam möglich ist, egal was im Prompt steht.
24. Als **Agent-nutzender Nutzer** will ich Opt-in-Etikette im Skill dokumentiert: Agent spricht nur auf meine Anweisung oder Standing-Order in der AGENTS.md, session-sticky bis Widerruf, damit keine Agentin von sich aus plappert.
25. Als **Agent-nutzender Nutzer** will ich, dass die Ansage nützlich ist (Status / nächster Schritt / wofür ich gebraucht werde), nicht die vorgelesene Langantwort, damit 10 Sekunden Ansage reichen.
26. Als **Fleet-Administrator** will ich statische Binaries von Gitea-Releases versionpinnt + sha256-verifiziert ausrollen (agent-lib/officecli-Muster), damit das Ansible-Rollout deterministisch ist.
27. Als **Fleet-Administrator** will ich Keys user-spezifisch als Env-Variablen (`TALK_*`) ausrollen nach unserem AZ_FLEET-Muster, damit kein Secret in Config-Dateien landet.
28. Als **Fleet-Administrator** will ich `talk doctor` als Diagnose-Output, damit „kein Ton bei Kollege X" in 5 Sekunden selbsterklärend ist.
29. Als **Fleet-Benutzer ohne Adminrechte** will ich, dass talk ohne Runtime-Abhängigkeiten (kein ffmpeg, kein Player, kein Python) läuft, damit es auf jedem Firmenrechner einfach funktioniert.
30. Als **Nutzer auf einem Rechner ohne laufenden Daemon** will ich einen Degraded Mode (Lock-File, sequenziell), damit talk auch ohne talkd nie kaputt ist.
31. Als **Privatnutzer mit NixOS-Hosts** will ich einen Flake-Output im Repo, damit meine Hosts talk wie gewohnt deklarativ beziehen.
32. Als **OS-Nutzer** will ich eine Desktop-Notification parallel zum Audio mit Abbrechen-Button, wo das OS es hergibt, damit ich stummschalten kann, ohne ein Terminal zu öffnen.
33. Als **Nicht-Terminal-Nutzer** will ich, dass TUI-Befehle ohne TTY auf plain Output zurückfallen, damit Pipes und Automatisierung niemals brechen.
34. Als **Projekt-Betreuer** will ich PR-getriebene Tests + Release-Matrix in Gitea Actions, damit jeder Tag automatisch konsistent gebaute Artefakte für alle Plattformen produziert.
## Implementation Decisions
- **Sprache/Laufzeit**: Go; statische Binaries für win/mac/linux × amd64/arm64. Keine Runtime-Abhängigkeiten.
- **Binaries**: `talk` (CLI) + `talkd` (Daemon). Repo: `code.m3ta.dev/m3tam3re/talk`, MIT-Lizenz.
- **Daemon**: talkd besitzt die Queue (Serialisierung, Prioritäten, Rate-Limit, Verwerfen bei Gedrängel), den persistenten Mute-State, die Ruhezeiten-Fenster (lokale Zeit, still verwerfen oder toast-only, konfigurierbar), die HTTP-API (`POST /speak` auf 127.0.0.1, CORS-Domains konfigurierbar) und einen MCP-Endpoint (Streamable HTTP, localhost).
- **Degraded Mode**: ohne laufenden talkd fällt das CLI auf Lock-File-Sequential zurück; nichts bricht still.
- **Audio**: embedded Playback (oto) — kein mpv/ffmpeg nötig. Audio-Sink wählbar inkl. `null` (CI, Server, doctor).
- **Notifications**: best effort über beeep (notify-send / macOS Notification Center / Windows Toast); Cancel-Button, wo das OS ihn hergibt.
- **Provider-Kette V1** (Auto-Fallback in dieser Reihenfolge, überschreibbar per Config/Flag): ElevenLabs → OpenAI TTS → Edge-TTS (frei, kein Key) → OS built-in (macOS say / Windows SAPI / Linux espeak).
- **Keys/Config**: Env-first (`TALK_ELEVENLABS_KEY`, `TALK_OPENAI_KEY`, …) plus optionale `config.toml` im platform-nativen Config-Dir (`~/.config/talk` / `~/Library/Application Support/talk` / `%APPDATA%\talk`). Provider-Endpunkte konfigurierbar (Testbarkeit, Firmenserver später). Per-User-Keys in V1; zentraler Firmenserver/Gateway bewusst später.
- **Sprache/Stimmen**: de/en-Heuristik (reine Funktion, kein ML); `voice.<provider>.<lang>`-Mapping in der config.toml; persönliche ElevenLabs-Stimme (KXxZ…) als privater Default, neutrale Defaults im Repo.
- **CLI-Verhalten**: non-blocking Default; Flags `--wait`, `--now` (Queue-Überprung für Dringendes), `--priority`, `--voice`, `--lang`, `--provider`. Befehle: say, wrap, mute, unmute, status, cancel, voices, test, doctor, setup.
- **TUI (Bubble Tea)**: interaktiv = setup (Wizard), status (Live-Dashboard), voices (Picker mit Preview); plain & scriptbar = say, wrap, mute, cancel, test, doctor; kein TTY → immer plain.
- **Etikette (Skill-Policy)**: striktes Opt-in — Sprechen nur auf Nutzer-Trigger des Skills oder Standing-Order in der AGENTS.md; session-sticky bis Widerruf (Prompt-Level, kein Daemon-Session-State); Ansageinhalt = Status/nächster Schritt/wofür der Nutzer gebraucht wird, nie die Langantwort. Daemon-Backstop: Rate-Limit + Mute + Ruhezeiten gelten unabhängig vom Prompt.
- **Verteilung**: GoReleaser → Gitea-Releases, Asset-Schema `talk_<os>_<arch>` + sha256 (agent-lib-Vertrag); Firma spiegelt intern (pin_mirror-Muster) + Fleet-Role nach agent-lib/officecli-Vorbild (Program Files + Machine-PATH, admin-geschützte ProgramData-Config); Skill-Rollout Firma via az-agent-defaults → agent-lib Three-State-Sync; privat via flake.nix.
- **CI**: Gitea Actions — PR → Tests, Tag → Release-Matrix.
- **Legacy-Abriss**: alte voice-notify/crunch-jobs-Skills (AGENTS-Repo) werden Redirect-Stubs; nixpkgs-PRs #24/#7 werden als superseded geschlossen; m3ta-nixpkgs talk/crunch nach Migration deprecatet; ElevenLabs-Key (exponiert in altem Prototyp-Skript) wird rotiert, danach das Skript gelöscht.
- **Naming**: Name bleibt `talk` (akzeptierte Kollision mit historischem Unix-talk; `say` vermieden wegen macOS).
## Testing Decisions
- **Guter Test** testet ausschließlich extern beobachtbares Verhalten an der Schnittstelle, nie Implementationsdetails.
- **Primäre Seam (eine)**: die Prozessgrenze von talkd — Daemon + CLI als Black Box, mit zwei über die Config injizierbaren Fakes: (1) Provider-Endpunkte zeigen im Test auf einen Fake-HTTP-Server mit Audio-Bytes; (2) `audio.sink = "null"` schaltet echtes Playback ab. An dieser Seam werden getestet: Queue-Serialisierung/Priorität/Rate-Limit/Verwerfen, Mute-Persistenz, Ruhezeiten, Fallback-Kette (Fake-Provider 500 → nächster Provider), non-blocking + `--wait`, Degraded Mode, HTTP-`/speak`-Vertrag inkl. CORS, MCP-Endpoint-Grundvertrag, status/cancel/doctor-Output.
- **Zwei reine Neben-Seams** (tischgesteuerte Unit-Tests, keine Mocks): Spracherkennung (de/en) und Config-Auflösung (Env + TOML-Merge).
- **Bewusst ungetestet** (hardware-/OS-gebunden): echtes Audio-Rendering und OS-Toasts — abgedeckt durch `talk doctor` als manuelles Diagnosewerkzeug.
- **Vorläufer**: keine — grüne Wiese. CI läuft von Ticket 1 an in Gitea Actions mit.
## Out of Scope
- LibreChat-Frontend-Anbindung (CORS-Tür wird gebaut, Integration selbst nicht)
- Browser-Extension als Brücke
- Zentraler Company-Gateway-Server (ein Key für alle)
- Timer/Scheduling (`crunch at|in|daily` bleibt OS-nativ: cron/systemd/Task Scheduler)
- Lokale Neural-Stimmen (Piper/Kokoro) — späterer Ausbau
- Kalenderbewusste Ruhezeiten (nur statische Zeitfenster in V1)
- Homebrew/Scoop/AUR-Feeds — später optional
- Mehrnutzer-Maschinen (Ein-Nutzer-pro-Host wie im Fleet)
## Further Notes
- **Pending (Nutzer)**: ElevenLabs-Key-Rotation im Dashboard VOR Löschung des alten Prototyp-Skripts.
- Persönlicher Default-Voice: ElevenLabs `KXxZd16DiBqt82nbarJx` (aus altem Stack übernommen als privater Default, nicht Repo-Default).
- Session-Stickiness der Etikette ist Prompt/Skill-Level — der Daemon kennt keine Agent-Sessions.
- Offene Später-Entscheidung: LibreChat-Anbindung via Frontend-Patch vs. Browser-Extension — blockiert nichts, Tür steht mit CORS-fähiger HTTP-API.
- Verwandte Firmen-ADRs: 0009 (agent-lib), 0012/0013 (Secrets-Env-Bridge/Machine-Env-Keys), 0016/0017 (user-scharfe Keys, MCP-Audience-Scoping) — talk folgt diesen Mustern.