Files
talk/.scratch/talk-2-0/plan.md
T

165 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 1: 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 2: 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 3: 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 4: 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 5: 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 6: 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 7: `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 8: `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 9: 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.