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

164 lines
21 KiB
Markdown
Raw Normal View History

# 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.