• v0.1.0 6490bafd23

    v0.1.0
    Some checks failed
    ci / server (push) Successful in 12s
    ci / desktop-ts (push) Successful in 28s
    ci / desktop-rust (push) Successful in 3m20s
    release / server-image (push) Failing after 11s
    Stable

    ahitosly released this 2026-09-05 16:23:20 +02:00 | 42 commits to main since this release

    Added

    • scaffold : monorepo, docs (SPEC, STATUS, DECISIONS, CHANGELOG, ECARTS, API, DATA, WORKFLOW), agents Claude Code, CI Forgejo, scripts (T-001)

    • server : /health, /v1/dictate (pipeline STT → profil → rewrite → fallback degraded), /v1/agent stub, vérif JWT HS256, 9 tests (T-001)

    • desktop : squelette Tauri v2 (overlay + settings), machine à états, hotkey, modules stubs, contrat src/lib/tauri.ts (T-001)

    • supabase : migration aria_init (5 tables aria_*, RLS, trigger profil), seed 20 apps (T-001)

    • supabase : migration aria_init + seed catalogue appliqués pour de vrai sur l'instance self-hosted partagée ; packages/contract/src/supabase.types.ts généré (T-005)

    • scripts : scripts/filter_supabase_types.py + gen-contract.sh filtrent désormais les types Supabase générés aux objets aria_* (instance partagée, cf. E-008) (T-005)

    • server : fallback JWKS pour la vérification JWT Supabase (clés asymétriques RS256/ES256, jwt.PyJWKClient avec cache), utilisé quand SUPABASE_JWT_SECRET est vide ; 20 tests avec tokens forgés (T-010)

    • server : couverture de tests du client Supabase clé service (SupabaseDB, aria_server/db/supabase.py) — lecture aria_app_catalog/aria_app_profiles/aria_profiles via PostgREST, 14 tests avec httpx.MockTransport (T-011)

    • server : couverture de tests de la résolution app → profil (aria_server/apps/resolver.py, docs/DATA.md §Résolution) — 10 tests avec catalogue/profils mockés en mémoire (tests/test_resolver.py) : app connue avec/sans profil dédié, custom_app_id, profil par défaut, prompt global implicite, rewrite_enabled, override llm_model, minuscules Windows, isolation par utilisateur (T-012)

    • server : couverture de tests du provider STT OpenRouter (OpenRouterSTT), 17 tests avec httpx.MockTransport, dont un test d'intégration optionnel sauté par défaut (opt-in humain requis, pas d'appel réseau réel) (T-013)

    • server : couverture de tests du provider LLM OpenRouter (OpenRouterLLM, chat completions), 14 tests avec httpx.MockTransport (requête sortante, extraction/strip du contenu, mapping de toutes les erreurs vers LLMError) ; couverture de tests du prompt système de base et de l'assemblage (aria_server/pipeline/prompts.py, build_system_prompt), 13 tests (tests/test_prompts.py) : style/hésitations/ponctuation/langue conservée, ordre base → profil → titre de fenêtre, fallback app_name/app_id (T-014)

    • server : light_cleanup() (aria_server/pipeline/dictate.py) — nettoyage léger sans LLM appliqué quand rewrite_enabled=False (docs/SPEC.md §5.3 point 5, portée tranchée en ADR-008 : normalisation des espaces uniquement) ; test end-to-end de l'échec STT via /v1/dictate (502 stt_failed, distinct du fallback degraded pour un échec LLM) ; 9 tests ajoutés (tests/test_dictate.py, tests/test_pipeline_dictate.py) (T-015)

    • server : couverture de tests des tâches de fond SupabaseDB.upsert_seen_app/insert_transcription (aria_seen_apps/aria_transcriptions via BackgroundTasks), 10 tests (tests/test_supabase_db.py, tests/test_dictate.py) : payload PostgREST, header Prefer: resolution=merge-duplicates sur l'upsert, no-op quand non configuré, upsert_seen_app systématique vs insert_transcription conditionné à aria_profiles.history_enabled, cohérence de l'historique avec le fallback degraded (T-016)

    • server : échappement des valeurs interpolées dans les filtres PostgREST de SupabaseDB (db/supabase.py) — _pg_array_element (littéral tableau cs.{…}) et _pg_filter_value (eq.…/or=(...)) quotent conditionnellement toute valeur contenant un caractère réservé (, { } ( ) " \, espace), appliqué à find_canonical_app/find_app_profile/find_default_profile/get_profile_settings ; 17 tests ajoutés (tests/test_supabase_db.py) ; résorption de E-010 (T-092)

    • server : validation stricte du format WAV (aria_server/api/wav.py, validate_wav) — PCM 16 bits / 16 kHz / mono conforme au contrat (docs/API.md), rejette 422 audio_not_wav/audio_invalid_format: … (mauvaise fréquence, canaux, profondeur, format non-PCM) plutôt que de laisser passer un WAV mal formé au provider STT ; 13 tests (tests/test_wav.py) (T-017)

    • server : STTTimeoutError (sous-classe de STTError) distingue un timeout provider STT d'un échec générique ; /v1/dictate et /v1/agent répondent 504 stt_timeout au lieu de 502 stt_failed dans ce cas ; 2 tests provider + 1 test route (T-017)

    • server : middleware limit_upload_size (main.py) — rejette 413 audio_too_large au vu du Content-Length avant même le parsing multipart quand l'en-tête est présent, en complément du contrôle post-lecture existant (_read_audio) ; 5 tests (tests/test_main_limits.py) (T-017)

    • server : couverture de tests dédiée de POST /v1/agent (tests/test_agent.py, 10 tests) — parité des garanties avec /v1/dictate (401/413/422/502/504) et absence d'effets de bord en v1 (pas d'appel LLM, pas de tâche de fond upsert_seen_app/insert_transcription, la route ne prenant ni background ni db) ; ancien test minimal test_agent_stub de tests/test_dictate.py retiré (doublon) (T-018)

    • desktop : couverture de tests de la logique de garde/transition de la machine à états (apps/desktop/src-tauri/src/session.rs) — can_start, is_recording, other_mode, is_too_short, should_reset_after_timeout, classify_result extraits en fonctions pures sans dépendance AppHandle et testés (8 tests) ; SessionState dérive désormais PartialEq ; tests d'intégration bout-en-bout (effets de bord Tauri réels via tauri::test) hors périmètre de cette tâche, cf. E-011/T-093 (T-030)

    • desktop : hotkey::parse_or_default (apps/desktop/src-tauri/src/hotkey.rs) retombe sur DEFAULT_HOTKEY (Control+Space, désormais pub const dans config.rs plutôt que dupliquée) si la hotkey stockée en config est invalide, au lieu de faire échouer register_main (et donc le démarrage de l'app) ; 10 tests unitaires purs ajoutés couvrant le parsing du défaut, le rejet des chaînes invalides, un combo custom valide, et le fallback (T-031)

    • desktop : capture micro réelle (apps/desktop/src-tauri/src/audio.rs, résorption partielle d'E-002) — cpal sur thread dédié (le Stream n'est jamais envoyé hors du thread), device sélectionné depuis config.rs (fallback + warning si absent/introuvable), tous les formats d'échantillon courants supportés, WAV PCM 16 bits/16 kHz mono conforme au contrat serveur ; RMS calculé toutes les 50 ms (RmsWindower) et poussé via session.tick, déjà câblé sur session://state.rms depuis T-030 ; logique de signal (downmix multi-canal, conversion de format, rééchantillonnage linéaire, calcul RMS, fenêtrage 50 ms) extraite en fonctions pures et testée, 16 tests ajoutés (T-032)

    • infra : docker-compose.yml (racine) + apps/server/.dockerignore — docker compose up --build construit et démarre apps/server seul (Dockerfile déjà scaffoldé en T-001), port 8787, env_file: .env, healthcheck GET /health ; testé en local (build + run + /health OK avec des valeurs d'env factices). Déploiement effectif sur le serveur de Malo (reverse proxy HTTPS, DNS) hors périmètre agent, laissé en vérification manuelle (T-020)

    • server : apps/server/tests/e2e/curl_dictate.sh paramétré (APP_ID/APP_NAME/WINDOW_TITLE/PLATFORM/LOCALE en env, défauts Outlook rétrocompatibles) pour cibler n'importe quel profil stocké en base sans éditer le script (T-021)

    • desktop : couverture de tests de config.rs (AriaConfig/ConfigPatch, déjà scaffoldé et fonctionnellement utilisé depuis T-036/T-037/T-094 mais sans aucun test) — logique de fusion d'un patch partiel extraite en fonction pure merge_patch (testable sans AppHandle/tauri-plugin-store, même approche que session.rs/hotkey.rs/audio.rs/api_client.rs) ; 10 tests couvrant les défauts, un patch vide, chaque champ isolément, la distinction Some(None)/None pour microphone (remise au périphérique par défaut vs champ non fourni), plusieurs champs à la fois, et le round-trip serde_json (contrat src/lib/tauri.ts) (T-038)

    • desktop : refresh de session Supabase côté Rust (auth.rs) — AuthStore::refresh appelle réellement POST {SUPABASE_URL}/auth/v1/token?grant_type=refresh_token quand le token expire dans moins de 60 s, sérialisé par un tokio::sync::Mutex (évite deux refresh concurrents sur un refresh token à usage unique) ; un rejet explicite (4xx) invalide la session et émet auth://expired, une panne réseau transitoire la conserve (AriaError::Network, pas de déconnexion forcée) ; 10 tests unitaires purs (détection d'expiration, parsing de la réponse GoTrue) ; résorption d'E-001 (T-036)

    • desktop : src-tauri/build.rs propage les clés VITE_* du .env racine du monorepo vers l'environnement de compilation Rust (cargo:rustc-env, parseur minimal fait main) — nécessaire pour que SUPABASE_URL/VITE_SUPABASE_ANON_KEY (auth.rs) et VITE_ARIA_SERVER_URL (config.rs, jamais réellement fonctionnel avant cette PR malgré sa présence depuis le scaffold) aient une valeur au runtime (T-036)

    • desktop : page Settings › Compte (src/settings/pages/Account.tsx, stub jusqu'ici) — login par mot de passe ou par code à 6 chiffres envoyé par email (signInWithOtp/verifyOtp, plutôt qu'un lien magique cliquable qui nécessiterait un deep link OS non posé par cette tâche), affichage de l'utilisateur connecté, déconnexion (signOut, propagée au core Rust via bindSessionToCore() déjà scaffoldée) (T-037)

    • desktop : windows.rs::bottom_center_position extraite en fonction pure et testée (5 tests) ; overlay confirmé conforme à docs/SPEC.md §4.1 (transparente, always-on-top, non focusable, skip taskbar, waveform 6 barres, libellés, couleur par mode, fondu 150 ms — déjà scaffoldé, aucune modification nécessaire côté Overlay.tsx/tokens.css) (T-033, T-034)

    • desktop : tray.rs — icône tray à 3 variantes (idle / active-recording-processing-injecting / erreur, monochrome template macOS, icons/tray-idle*.png/tray-recording*.png/tray-error*.png générées depuis 3 nouveaux SVG), mise à jour à chaque changement de session://state (TrayIcon::set_icon_with_as_template, API Tauri de haut niveau marshalée en interne vers le thread principal — pas d'appel objc2/AppKit direct) ; menu tray "Ouvrir Aria" / "Activer-Désactiver la dictée" / séparateur / "Quitter" — le nouvel item bascule un drapeau global (hotkey::ENABLED) qui empêche la hotkey principale de démarrer un nouvel enregistrement tant qu'il est désactivé (sans jamais avaler la frappe dans ce cas, pour que Ctrl+Space reste utilisable normalement dans l'app active), un enregistrement déjà en cours va toujours à son terme ; fenêtre settings cachée au démarrage déjà confirmée conforme depuis T-003 (aucune modification nécessaire) ; 4 tests ajoutés (tray.rs : sélection d'icône par état, libellé du menu, validité/distinction des 3 PNG ; hotkey.rs : toggle) (T-039)

    • desktop : api_client.rs — 1 retry (délai 500 ms) sur erreur réseau uniquement (connexion/DNS/timeout, pas un 401/erreur serveur) pour POST /v1/dictate//v1/agent ; corps multipart reconstruit à chaque tentative (consommé par l'envoi) ; timeout de requête (45 s) inchangé, extrait en constante ; décision de retry extraite en fonction pure should_retry testée sans réseau (6 tests) (T-035)

    • desktop : permissions macOS (permissions.rs, nouveau module) — check_permissions (commands.rs) n'est plus un stub codé en dur : détection réelle Microphone (AVCaptureDevice.authorizationStatusForMediaType) et Accessibilité (AXIsProcessTrusted, sans invite système) sur macOS, toujours accordé sur Windows/Linux (docs/SPEC.md §4.5) ; nouvelle commande open_permission_settings ouvre directement le sous-panneau Réglages Système concerné (x-apple.systempreferences:…?Privacy_Microphone/Privacy_Accessibility, via tauri_plugin_opener::open_url côté Rust) ; nouvel onglet Settings « Permissions » (src/settings/pages/Permissions.tsx) affichant l'état de chaque permission avec bouton d'ouverture des réglages et « Revérifier », sélectionné automatiquement au montage si une permission manque ; la fenêtre Settings s'ouvre désormais automatiquement au démarrage sur macOS si une permission manque. Threading vérifié avant tout code (ni authorizationStatusForMediaType ni AXIsProcessTrusted ne sont main-thread-only, à la différence des pièges TSM/TIS de T-094/T-096 ou de NSWindow::setLevel en T-098) — revérifié par un smoke-test 4-threads réel en plus de la lecture des sources objc2-av-foundation/tauri. Confirmé physiquement par l'humain (2026-09-05) : détection, ouverture du bon sous-panneau Réglages Système, revérification et cas nominal tous conformes (T-040)

    • process : T-054 vérifiée sans modification de code — le contexte envoyé au serveur (api_client.rs::Context : app_id/app_name/window_title/platform/locale) était déjà entièrement sérialisé et transmis depuis le scaffold, conforme à docs/API.md ; seul son contenu était inutilisable côté macOS tant qu'active_app.rs était un stub (T-050 le rend désormais utile)

    • desktop : injector.rs::paste distingue désormais texte / image / vide au moment de restaurer le clipboard après collage (snapshot/restore, arboard::get_image/set_image/clear) — un clipboard qui contenait une image ou qui était réellement vide n'est plus laissé avec le texte injecté en permanence après une dictée (T-052, ADR-004). T-053 (refocus) : vérifié sans modification de code que l'overlay ne prend jamais le focus (focus:false, aucun set_focus() sur ce chemin) — l'app cible garde le focus du système pendant tout le cycle

    • process : critère M2 validé (T-041) — maintenir la touche → overlay → relâcher → texte reçu visible dans les logs et l'overlay, formalisé à partir des confirmations physiques déjà obtenues sur les cycles T-094 (press/release fiable)/T-033-T-034/T-098 (overlay visible, positionné, au-dessus de toute fenêtre)/T-095-T-096-T-097 (texte reçu → injecté sans crash ni bip) ; « visible dans l'overlay » interprété comme "l'overlay reflète l'état de réception/injection" (conforme à docs/SPEC.md §4.1, l'overlay n'affiche jamais le texte lui-même). M2 clos — T-093/E-011 (généricité R: tauri::Runtime) et T-100/E-018 (icône tray idle invisible) restent ouvertes pour un futur cycle, ni l'une ni l'autre n'étant un prérequis du critère

    • desktop : session.rs::stop_and_process journalise désormais le texte reçu en toutes lettres (log::info!, niveau par défaut) en plus de sa longueur, pour que « texte reçu visible dans les logs » (critère M2, T-041) soit littéralement vrai sans configuration de log supplémentaire (T-041)

    • desktop : active_app.rs macOS (résorption partielle d'E-003) — NSWorkspace::sharedWorkspace().frontmostApplication() (objc2-app-kit) pour le bundle id/nom de l'app au premier plan, AXUIElement (objc2-application-services, Accessibilité T-040 requise) pour le titre de la fenêtre focus ; capturé depuis le thread CGEventTap de la hotkey (session.rs::SessionManager::start()), pas le thread principal — vérifié documentairement (aucune des API utilisées n'est MainThreadOnly) et empiriquement (test dédié exécuté réellement sur macOS, résultat correct). E-007 résorbé au passage (objc2/objc2-app-kit/objc2-foundation alignés sur ce que tauri/wry tirent déjà en transitif, 0.5/0.2 → 0.6/0.3) ; résiduel non bloquant documenté (objc2 0.5.2 via enigo→icrate, hors périmètre). Log ajouté (session.rs::start) après un premier test humain sans log visible (capture faite mais jamais journalisée). Confirmé physiquement par l'humain (com.apple.TextEdit/TextEdit/nom du document réels) (T-050)

    • process : T-060 vérifiée sans modification de code — le trigger Supabase de création automatique de aria_profiles à l'inscription (aria_handle_new_user/aria_on_auth_user_created) existait déjà dans la migration initiale (20260902120000_aria_init.sql, appliquée pour de vrai dès T-005) ; aucune migration 0002 nécessaire, comportement déjà éprouvé par les logins réels T-036/T-037/T-095/T-096/T-097 (T-060)

    • supabase : seed/aria_app_catalog.sql étendu de 20 à 30 apps — Discord, Signal, Telegram, Zoom, Linear, Figma, Sublime Text, PowerPoint, Aperçu, Calendrier, chacune avec suggested_prompt ; Gmail/Notion/Linear-web/Jira-web restent hors scope (indistinguables d'un navigateur, docs/SPEC.md §12). Validé par dry-run réel contre l'instance partagée (30 lignes, rollback). Application réelle en attente (cf. docs/STATUS.md → Vérifications manuelles) (T-061)

    • desktop : Settings › Applications (src/settings/pages/Applications.tsx, T-062+T-064 dans la même PR) — CRUD des profils de réécriture par app (aria_app_profiles) via src/lib/profiles.ts (hooks TanStack Query + supabase-js typés) et src/components/profile-form.tsx (formulaire réutilisable) : profil par défaut éditable, apps détectées (aria_seen_apps) avec correspondance catalogue côté client pour l'affichage, catalogue des apps non encore détectées, prompt suggéré préempli à la création, toggle rewrite_enabled, modèle optionnel. Résumé statique des règles de BASE_STYLE affiché en lecture (pas une lecture live du serveur, cf. E-020/T-101) (T-062, T-064)

    • desktop : injector.rs::paste_modifier extrait en fonction pure + 3 tests (T-068) — couverture automatisée absente jusqu'ici (arboard::Clipboard::new() a besoin d'un vrai clipboard système, indisponible en CI), dont une régression explicite d'E-014 (Key::Unicode('v') sur macOS) ; checklist manuelle M4 (apps/desktop/tests/manual/M4.md, T-068)

    • desktop : Settings › Avancé (src/settings/pages/Advanced.tsx, T-066) — version (getVersion), vérification manuelle de mise à jour (check(), sans installation — flux complet en T-081/M6), timings/statut de la dernière dictée (nouveau diagnostics.rs, alimenté par api_client.rs), logs récents (nouveau logbuffer.rs, enveloppe env_logger::Logger existant dans un tampon circulaire de 200 lignes sans changer la sortie stdout) (T-066)

    • desktop : Settings › Historique (src/settings/pages/History.tsx, src/lib/history.ts, T-065) — liste des dictées (aria_transcriptions, limite 200), recherche client, suppression individuelle/totale, toggle aria_profiles.history_enabled (T-065)

    • desktop : Settings › Général (src/settings/pages/General.tsx, T-063) — capture de hotkey (combinaison modificateur(s)+touche compatible hotkey.rs::parse), choix du microphone (listMicrophones) avec option explicite "périphérique par défaut", langue, URL serveur ; s'appuie sur getConfig/setConfig/onConfigChanged déjà posés (T-038) (T-063)

    • desktop : src/components/ui/{button,input,textarea,select,switch,label,card}.tsx — premières primitives réutilisables du design system (T-067), style shadcn (class-variance-authority/cn) posé sur les tokens --aria-* existants ; 2 tokens ajoutés (--aria-surface, --aria-success, variantes clair/sombre) ; thème clair/sombre « suit l'OS » déjà conforme depuis le scaffold, aucune modification nécessaire sur ce point (T-067)

    • process : T-070/T-071 vérifiées sans modification de code (M5, mode agent) — Tab pendant Recording bascule déjà Mode::Dictation ↔ Mode::Agent (session.rs::toggle_mode, hotkey.rs::register_recording_keys) et l'overlay reflète déjà le mode (libellé "Agent", point de couleur --aria-agent violet vs --aria-accent bleu) depuis T-030/T-033/T-034 ; le client envoie déjà POST /v1/agent en mode agent et affiche son status dans l'overlay sans jamais injecter (api_client.rs::send, session.rs::classify_result → ProcessOutcome::ShowMessage) depuis T-030/T-035/T-066 (T-070, T-071)

    • server : AgentResult étendu — nouveau modèle AgentAction (type/label/payload libres, forme non figée tant que Hermes/T-073 n'est pas branché) et champs actions: list[AgentAction]/needs_confirmation: bool (défauts vides/faux), purement additif, aucun changement de comportement de /v1/agent en v1 ; docs/API.md documente la forme exacte ; 5 tests ajoutés (tests/test_schemas.py) + 2 assertions dans test_agent.py::test_agent_stub (T-072)

    • server : /v1/agent branché sur Hermes (T-073) — nouveau HermesAgentProvider (aria_server/providers/agent/hermes.py, POST {HERMES_URL}/v1/responses, choix d'endpoint documenté en ADR-010) et pipeline/agent.py::run_agent (STT → Hermes si HERMES_URL/HERMES_API_KEY configurés, sinon reste "not_implemented" comme avant T-073) ; mapping output[] → AgentResult (message/output_text → message assemblé, function_call → un AgentAction par item, arguments JSON-parsé) ; status="needs_confirmation"/needs_confirmation=true dès qu'au moins une action est proposée (aucune n'est exécutée par Aria en v1, cf. ADR-010), sinon "done" ; nouvelles AgentError/AgentTimeoutError → 502 agent_failed/504 agent_timeout sur échec Hermes (réseau, timeout, HTTP non-2xx, JSON invalide) — volontairement une erreur explicite plutôt qu'un fallback dégradé silencieux (à la différence de la réécriture LLM en dictée, ADR-008), un échec Hermes ne laissant rien d'exploitable côté client ; nouveau prompt système dédié build_agent_system_prompt (pipeline/prompts.py, distinct du prompt de dictée) ; docs/API.md §/v1/agent réécrit. 43 tests ajoutés (tests/test_hermes_agent.py : mapping/erreurs du provider avec httpx.MockTransport ; tests/test_agent.py : comportement de la route Hermes configurée/non configurée avec FakeAgent). Testé en conditions réelles contre le vrai Hermes (provider isolé + pipeline serveur complet via TestClient, STT mocké) — voir docs/STATUS.md T-073 pour le détail. tests/e2e/curl_agent.sh ajouté (miroir de curl_dictate.sh) (T-073)

    • supabase : migration aria_agent_sessions (T-103) — une ligne par utilisateur (hermes_response_id, updated_at), RLS deny-all pour anon/authenticated (seul le serveur, clé service, y accède) ; mémorise la conversation Hermes active pour la continuité de conversation du mode agent. Approuvée après dry-run réel contre l'instance partagée, mergée (T-103)

    • server : continuité de conversation du mode agent (T-103, ADR-012) — pipeline/agent.py::run_agent/stream_agent_events lisent aria_agent_sessions avant d'appeler Hermes ; si la dernière interaction date de moins de 2h, previous_response_id est transmis pour poursuivre la même conversation, sinon nouvelle conversation vierge ; la conversation est mise à jour après chaque réponse Hermes réussie (tâche de fond sur /v1/agent, await direct en fin de flux sur /v1/agent/stream). HermesAgentProvider.run/run_stream envoient désormais store: true (au lieu de false) et acceptent previous_response_id ; AgentProviderResult.response_id (nouveau). 24 tests ajoutés (tests/test_supabase_db.py, tests/test_pipeline_agent.py nouveau, tests/test_hermes_agent.py, tests/test_agent.py) (T-103)

    • server : POST /v1/agent/stream (T-102, ADR-012) — miroir streaming de /v1/agent (text/event-stream), additif (/v1/agent JSON inchangé). HermesAgentProvider.run_stream appelle Hermes avec stream: true/store: true, parseur SSE minimal (_parse_sse_lines, fonction pure) ; pipeline/agent.py::stream_agent_events normalise le flux brut d'Hermes en événements Aria ({"type": "token"/"tool"/"done"/"error", ...}) via _map_sse_event (fonction pure, réutilise le mapping message/actions déjà testé en T-073) — function_call_output volontairement non transmis au client. Une fois les en-têtes 200 text/event-stream envoyés, une erreur STT/Hermes devient un événement error dans le flux plutôt qu'un statut HTTP. 32 tests ajoutés (tests/test_hermes_agent.py, tests/test_pipeline_agent_stream.py nouveau, tests/test_agent_stream.py nouveau). Testé en conditions réelles contre le vrai Hermes (streaming de tokens + visibilité outil + persistance de la continuité confirmés) (T-102)

    • desktop : mode agent en streaming (T-102) — api_client.rs::send_agent_stream consomme POST /v1/agent/stream (reqwest::Response::chunk(), pas de nouvelle dépendance) ; drain_sse_events/parse_sse_frame (fonctions pures) parsent le flux SSE en StreamEvent (Token/Tool/Done/Error, miroir du contrat serveur) ; render_stream_progress/render_stream_done (fonctions pures) construisent le texte affiché (une ligne 🔧 <outil> par outil démarré + texte accumulé, repli sur format_agent_display si aucun token reçu). session.rs::stop_and_process (mode Agent) route désormais vers le flux au lieu de l'ancien appel JSON unique à /v1/agent, met à jour SessionState::AgentMessage à chaque événement (aucun nouveau canal IPC, session://state déjà émis à chaque mise à jour) ; comportement inchangé sur l'événement final (pas d'injection, reset après 6 s). ProcessOutcome::ShowMessage retiré (classify_result ne gère plus que la dictée, seul appelant restant). 22 tests ajoutés (T-102)

    • desktop : updater automatique (T-081) — updater.rs::spawn_periodic_check (tauri-plugin-updater, déjà enregistré depuis le scaffold) check au démarrage puis toutes les 6h, émet update://available (contrat déclaré côté WebView depuis T-001, jamais câblé côté Rust avant cette tâche) ; UpdateBanner.tsx (nouveau, persistant dans settings/App.tsx) propose « Installer et redémarrer » (downloadAndInstall() + relaunch()) ou « Ignorer ». pubkey (tauri.conf.json) reste un placeholder tant que T-008 n'est pas faite : le mécanisme échoue donc systématiquement aujourd'hui (signature non vérifiable), géré comme une erreur normale (loggué côté Rust, affiché sans crash côté UI), non testable en conditions réelles avant T-008

    • server/desktop : GET /v1/prompts/base (T-101, résorption E-020) — expose BASE_STYLE (pipeline/prompts.py) tel quel, authentifié comme les autres routes /v1. Settings › Applications l'affiche désormais en direct (api_client.rs::get_base_prompt → commande get_base_prompt → lib/tauri.ts::getBasePrompt()) à la place du résumé statique fait main (GLOBAL_PROMPT_SUMMARY, supprimé), qui pouvait dériver du texte réel. 3 tests serveur ajoutés (tests/test_prompts_base.py)

    • scripts : scripts/release.sh (T-080, ADR-005) — phase 1 (vX.Y.Z) bump les versions (package.json/tauri.conf.json/Cargo.toml+Cargo.lock/pyproject.toml/aria_server/__init__.py::__version__) et bascule docs/CHANGELOG.md, ouvre une PR (scripts/pr.sh, jamais de push direct sur main) ; phase 2 (--publish vX.Y.Z, après merge de cette PR) tague, build (pnpm tauri build), crée la release Forgejo, upload les installeurs trouvés, et désormais la signature updater : TAURI_SIGNING_PRIVATE_KEY/_PASSWORD (T-008, renseignées par l'humain ce cycle) sont consommées nativement par tauri build (confirmé, pas deviné) ; les .sig produits sont assemblés en latest.json (format "Static" de tauri-plugin-updater, confirmé en lisant la crate vendue) et publiés comme asset d'une release Forgejo séparée taguée littéralement latest (créée si absente, fusionnée si un latest.json d'une plateforme précédente existe déjà) — hypothèse sur le comportement Forgejo documentée explicitement, non vérifiée par un vrai --publish. Toujours non exécuté pour de vrai (créerait un vrai tag/une vraie release) : seule la logique pure (bump, assemblage latest.json, détection de cible updater) a été vérifiée sur des fichiers scratch — trois bugs shell/Python réels y ont été trouvés et corrigés à cette occasion (heredoc+pipe vers le même stdin, un vrai bug de parsing de bash 3.2 avec heredoc imbriqué dans une substitution de processus, f-string avec guillemet échappé incompatible avec Python < 3.12), cf. docs/STATUS.md T-080 pour le détail. PR laissée en review, approbation humaine requise (release, CLAUDE.md règle 3b)

    Changed

    • desktop : clé de signature updater (minisign) générée par l'humain, clé publique renseignée dans apps/desktop/src-tauri/tauri.conf.json → plugins.updater.pubkey (remplace le placeholder REPLACE_WITH_MINISIGN_PUBLIC_KEY) ; clé privée conservée hors repo. Résorption de T-008. Reste à faire avant une release réellement signée : TAURI_SIGNING_PRIVATE_KEY/TAURI_SIGNING_PRIVATE_KEY_PASSWORD dans le .env local (secrets, non renseignés par un agent) et le câblage de l'étape de signature dans scripts/release.sh (T-080)
    • desktop : SessionManager/ConfigStore/AuthStore/AppState (+ les fonctions libres qu'ils appellent avec un AppHandle : windows.rs, hotkey.rs, audio.rs::start, tray.rs::update_icon, api_client.rs::send/send_agent_stream/get_base_prompt) rendus génériques sur R: tauri::Runtime (AppState<R = tauri::Wry>, défaut inchangé pour tout appelant existant) — permet des tests d'intégration de la machine à états via tauri::test::mock_builder()/MockRuntime ; 3 tests ajoutés (session.rs::integration_tests, macOS uniquement) exerçant cancel()/toggle_mode()/stop_and_process() par la vraie API publique avec capture réelle des événements session://state, sans toucher au micro/réseau réels. Aucun changement de comportement en production (résorption d'E-011) (T-093)
    • infra : apps/server déployé sur https://aria-api.temp.cloud-rennais.fr/ (reverse proxy HTTPS, action humaine) ; /health confirmé joignable depuis l'extérieur du LAN (T-020)
    • process : critère M1 validé en conditions réelles — curl (wav réel via say, mono/16kHz/16-bit) + JWT Supabase réel + POST /v1/dictate contre le serveur déployé → final_text réécrit selon un profil réel en base (custom_app_id=com.apple.TextEdit, Outlook non disponible sur la machine de test), degraded:false, timings renseignés ; appel OpenRouter réel STT+LLM autorisé explicitement (T-021)
    • server : run_dictation (pipeline/dictate.py) — quand rewrite_enabled=False, final_text passe désormais par light_cleanup() (normalisation des espaces) au lieu de renvoyer raw_text tel quel (T-015)
    • workflow : orchestrateur point d'entrée unique, review + merge automatiques, vérifications manuelles tracées dans STATUS sans bloquer (T-090)
    • ci : workflows sur runner forge-runner label docker (T-006)
    • process : validation end-to-end de scripts/pr.sh (création + merge programmé) via l'API Forgejo réelle, FORGEJO_TOKEN désormais disponible (T-007)

    Fixed

    • desktop : les tableaux markdown (syntaxe GFM \| … \|) dans les réponses de la fenêtre agent s'affichaient en texte brut au lieu d'un vrai tableau — react-markdown (T-105) ne supporte que CommonMark par défaut, qui n'inclut pas les tableaux (extension GitHub-Flavored Markdown). Nouvelle dépendance remark-gfm, composants table/th/td dédiés (bordures, wrapper overflow-x-auto) (T-108)
    • desktop/server : le flux /v1/agent/stream était coupé en plein milieu d'une réponse Hermes en cours (ex. pendant un outil/recherche web) — un timeout de durée totale de requête (reqwest::ClientBuilder::timeout, 45 s côté client ; timeout httpx unique côté serveur, 30 s, appliqué aussi à read) ne tolérait pas un silence SSE prolongé pourtant normal. Corrigé : côté client, plus de timeout global sur ce flux, un timeout de lecture par chunk (200 s, réappliqué à chaque itération) à la place ; côté serveur, HermesAgentProvider utilise désormais un httpx.Timeout dédié (hermes_timeout_s, 180 s de read, connect/write courts inchangés), distinct de provider_timeout_s (STT/LLM). La fenêtre agent ne se réaffichait pas non plus après une fermeture via la croix native (bouton rouge macOS), contrairement à Esc — la croix déclenche WindowEvent::CloseRequested, non intercepté, donc la webview était détruite (comportement par défaut de Tauri) au lieu d'être cachée ; windows::install_agent_close_guard intercepte désormais cet événement pour la fenêtre agent et la cache (prevent_close + hide), même sémantique que Esc quel que soit le déclencheur (T-107)
    • desktop : chaque token/outil du mode agent s'affichait en double — useEffect s'abonnant à un événement Tauri (Promise<UnlistenFn>) sans garde contre un démontage précoce (React 18 StrictMode, laisse un abonnement orphelin en dev) ; nouvelle fonction subscribe() (lib/tauri.ts), appliquée à AgentWindow.tsx et Overlay.tsx (même motif latent, invisible côté overlay). La fenêtre agent ne se réaffichait plus après une fermeture (Esc) puis une nouvelle interaction — retrait de raise_window_level (NSPopUpMenuWindowLevel, pensé pour un cycle de vie court, hypothèse pas une fenêtre cachée/réaffichée en boucle) pour cette fenêtre, alwaysOnTop suffit (T-106)
    • desktop/server : dans la fenêtre agent (T-104), les outils utilisés par Hermes s'affichaient toujours avant tout le texte, même utilisés au milieu de la réponse — StreamSegment/push_stream_token/push_stream_tool/render_stream_segments (api_client.rs) préservent désormais l'ordre chronologique exact ; nouveau événement serveur {"type": "transcript", "text": ...} (pipeline/agent.py) affiche un vrai tour "utilisateur" ; Ctrl+Space pour continuer la conversation (T-103) effaçait la réponse précédente — l'historique des tours est désormais construit côté front (AgentWindow.tsx) via un nouveau canal agent://event, jamais effacé sauf Esc ; rendu markdown des réponses (react-markdown) au lieu de texte brut (T-105)
    • server : POST /v1/agent/stream n'affichait aucun contenu progressif derrière le reverse proxy déployé (buffering réseau par défaut) — en-têtes X-Accel-Buffering: no/Cache-Control: no-cache ajoutés à la StreamingResponse (T-104)
    • desktop : la réponse du mode agent était illisible dans la pilule de l'overlay (une ligne tronquée) — nouvelle fenêtre dédiée agent (centrée, redimensionnable, scrollable), ouverte dès le début du traitement, fermée uniquement sur Esc ; l'overlay ne réaffiche plus le contenu de la réponse (session.rs::should_show_overlay) (T-104)
    • desktop : le mode agent n'affichait que Agent : done (le statut brut) au lieu du message/des actions Hermes réels, et en rouge (état Error réutilisé) même en cas de succès — api_client.rs désérialise désormais message/actions[], les assemble (format_agent_display, une ligne 🔧 libellé par action puis le message, 5 tests) ; nouvel état SessionState::AgentMessage distinct d'Error dans l'overlay (violet, 6 s d'affichage au lieu de 2 s). Découvert et corrigé après le premier test réel bout-en-bout de T-073 (T-071)
    • desktop : Tab (bascule mode agent) et Esc (annulation) pendant l'enregistrement ne fonctionnaient jamais sur macOS — tauri-plugin-global-shortcut/Carbon RegisterEventHotKey ne préempte pas fiablement un raccourci sans modificateur, l'app au premier plan consommait la touche (un vrai caractère de tabulation s'insérait dans le document actif). Tab/Esc surveillés désormais dans le CGEventTap déjà actif pour la hotkey principale (hotkey.rs::macos_main, nouvel état RecordingKeysState), avalés pendant Recording uniquement. Windows/Linux inchangés (T-070, E-021, ADR-011)
    • desktop : api_client.rs::send_once ne journalisait que le code HTTP d'une réponse d'erreur (HTTP 502), jamais le corps (detail, ex. agent_failed/stt_failed/agent_timeout) — impossible de diagnostiquer depuis les logs client lequel de STT ou de l'agent (Hermes) avait échoué. Le corps de réponse est désormais loggué (log::warn!) en plus du code
    • desktop : config.rs::ConfigPatch.microphone (Option<Option<String>>) collapsait un patch JSON {"microphone": null} (envoyé par la WebView pour remettre le micro au périphérique par défaut) en None/« champ non fourni » au lieu de Some(None)/« explicitement remis à zéro » — remettre le micro au défaut depuis les settings était un no-op silencieux depuis T-038, jamais exercé par un test passant par le vrai JSON (les tests existants construisaient ConfigPatch directement en Rust). Corrigé par un deserialize_with fait main (some_option), 4 tests ajoutés couvrant le JSON réel. Découvert en écrivant Settings › Général (T-063)
    • desktop : CI desktop-rust rouge sur main depuis T-040 (cargo clippy -- -D warnings, dead_code sur permissions.rs::settings_pane_url, non détecté localement faute d'avoir utilisé la commande exacte de la CI) — settings_pane_url (et son test) passés sous #[cfg(target_os = "macos")], seule cible où la fonction a un appelant hors test ; scripts/check.sh --rust (commande CI) revérifié vert (E-019, résorbé)
    • desktop : icône tray toujours invisible à idle après T-099 (confirmé par l'humain, correctif insuffisant) — tray.rs::setup() pose désormais l'icône initiale via update_icon() (même chemin de code que toute mise à jour ultérieure) au lieu du builder TrayIconBuilder::icon()/.icon_as_template() à la construction, qui semble ne pas appliquer correctement le flag "template" macOS. Hypothèse infirmée (toujours invisible, confirmé par l'humain) — code conservé (amélioration légitime, un seul chemin de code désormais), cause réelle reportée à plus tard sur décision explicite de l'humain (T-100, E-018 ouvert — E-017/T-099 insuffisant)
    • desktop : overlay macOS relevé à NSPopUpMenuWindowLevel (windows.rs::raise_overlay_level, objc2-app-kit) au lieu du NSFloatingWindowLevel par défaut de alwaysOnTop — il apparaissait derrière un document ouvert au premier plan. Appel dispatché via WebviewWindow::run_on_main_thread après un premier crash confirmé (setLevel appelé depuis le thread CGEventTap de la hotkey, AppKit exige le thread principal). Confirmé physiquement : visible au-dessus, plus de crash. position_overlay utilise désormais monitor.work_area() (exclut Dock/barre de menu, NSScreen.visibleFrame()) au lieu des bornes brutes de l'écran — l'overlay chevauchait visuellement le Dock, retour utilisateur immédiat. Confirmé physiquement (T-098, E-016)
    • desktop : hotkey.rs (macos_main) avale désormais la touche principale du combo (CGEventTapOptions::Default + CallbackResult::Drop) au lieu de seulement l'observer (ListenOnly) — l'auto-repeat clavier laissait fuir un KeyDown répété vers l'app au premier plan, provoquant un bip macOS à chaque répétition quand elle n'a pas de raccourci pour cette combinaison. Seule la touche principale est avalée (jamais les modificateurs, jamais hors combo). Confirmé physiquement par l'humain : plus de bip, dictée toujours fonctionnelle (T-097, E-015)
    • desktop : injector.rs::send_paste sur macOS n'utilise plus enigo::Key::Unicode('v') (résolution de keycode dépendante du layout clavier, appelait l'API TIS/TSM macOS et crashait depuis le thread tokio de session.rs::stop_and_process — même contrainte que E-012, cf. hotkey.rs) mais Key::Other(KeyCode::ANSI_V), un keycode brut qui contourne entièrement ce chemin de code. Autres OS inchangés. Découvert au premier cycle de dictée complet réussi (T-095 + T-096). Vérification physique humaine (collage réel sans crash) en attente (T-096, E-014)
    • server : decode_supabase_jwt (aria_server/auth.py) route désormais sur l'algorithme réel du JWT (RS256/ES256 → toujours JWKS) au lieu de choisir HS256 dès qu'un secret est configuré — un vrai token de session Supabase self-hosted (ES256) était rejeté en 401 malgré un secret HS256 legacy aussi présent ; découvert au premier login réel de bout en bout, 5 tests ajoutés. Nécessite un redéploiement du serveur pour prendre effet (T-095, E-013)
    • desktop : logs de diagnostic supplémentaires (auth::set, auth::access_token, api_client sur 401) pour distinguer une session locale absente d'un rejet serveur — ont permis de diagnostiquer T-095 (T-095)
    • desktop : windows.rs::position_overlay positionne désormais l'overlay sur l'écran qui contient le curseur (active_monitor, via cursor_position()/monitor_from_point()) au lieu du dernier écran connu de la fenêtre elle-même (current_monitor() seul), avec repli sur le moniteur courant puis primaire ; résorption d'E-005. Vérification physique multi-écrans en attente (T-033, E-005)
    • desktop : hotkey principale (Control+Space) sur macOS détectée via un CGEventTap maison (crate core-graphics, #[cfg(target_os = "macos")], hotkey.rs) au lieu de tauri-plugin-global-shortcut (qui n'émettait jamais Released) ou rdev (qui plantait) — ne lit que le keycode brut et les flags de modificateurs, jamais de conversion en chaîne via une API TIS/TSM. Autres OS inchangés (tauri-plugin-global-shortcut conservé). 14 tests sur la logique pure de détection de front/combo. Confirmé physiquement par l'humain : relâchement détecté à chaque essai, aucun crash (E-012 résorbé, ADR-009) (T-094)
    • desktop : revert du correctif rdev pour la hotkey principale (T-094) — causait un crash systématique (EXC_BREAKPOINT, dispatch_assert_queue_fail dans une API macOS TIS/TSM appelée hors thread principal par rdev), constaté en conditions réelles. Retour à tauri-plugin-global-shortcut seul : Pressed fonctionne, Released reste non fiable en attendant un correctif CGEventTap maison (E-012, ADR-009)
    • desktop : ajout de logs manquants sur le chemin d'arrêt de la dictée (audio::stop — nombre d'échantillons ; session::stop_and_process — enregistrement trop court ignoré, dictée injectée, statut agent) — jusqu'ici seul audio start était loggué, rendant le relâchement de la hotkey silencieux en l'absence d'overlay (T-033/034 pas encore posés), constaté lors d'un test manuel utilisateur (T-032)
    • server : SupabaseDB.upsert_seen_app/insert_transcription attrapent et logguent (WARNING, aria.db) désormais toute erreur HTTP/réseau au lieu de la laisser remonter — ces écritures tournent en BackgroundTasks après l'envoi de la réponse /v1/dictate, une exception non gérée y serait invisible pour le client et seulement tracée en erreur non gérée par FastAPI (T-016)
    • server : OpenRouterSTT.transcribe convertit désormais un corps de réponse non-JSON ou JSON non-objet en STTError (→ 502 stt_failed) au lieu de laisser fuiter une exception non catchée (→ 500) (T-013)
    • server : OpenRouterLLM.rewrite convertit désormais un corps de réponse non-JSON en LLMError (ValueError/json.JSONDecodeError ajoutés au except) au lieu de laisser fuiter une exception non catchée ; LLMError reste absorbée par le pipeline en degraded=True, jamais mappée à un statut HTTP (T-014)
    • ci : ci.yml — server utilise uv python install 3.12 au lieu de actions/setup-python@v5, desktop-ts ne fixe plus de version pnpm en double avec package.json, desktop-rust retire sudo (absent du conteneur) ; cache uv activé (T-009)
    • server : validation du bootstrap (uv sync, uv run pytest — 9/9 verts, ruff format/ruff check clean, /health répond) ; aucun correctif de code nécessaire, le scaffold M0 tournait déjà (T-002)
    • desktop : bootstrap macOS — génération des icônes app (pnpm tauri icon) et tray (tray.png/tray@2x.png depuis tray.svg, absents du scaffold) qui faisaient paniquer generate_context!() ; cargo check/clippy -D warnings/test/build, tsc --noEmit, eslint verts sans correctif de code ; pnpm tauri dev démarre (T-003)
    • server : MultiPartParser.spool_max_size (Starlette, 1 Mo par défaut) relevé au démarrage au-delà de max_audio_bytes — au-delà de ce seuil, Starlette écrivait silencieusement tout fichier uploadé sur un fichier temporaire disque (SpooledTemporaryFile), en contradiction avec la règle "l'audio n'est jamais écrit sur disque" dès qu'une dictée dépassait ~31 s ; vérifié qu'aucun fichier temporaire n'apparaît plus pour un audio de ~1,8 Mo (T-017)
    • server : docs/API.md §/v1/agent complété avec les codes d'erreur (401/413/422/502/504, identiques à /v1/dictate) et la précision llm_ms toujours null/pas de tâche de fond en v1 — absents du document avant cette PR, clarification des detail uniquement, pas de changement de forme d'openapi.json (T-018)
    • server : tests/conftest.py::client fixait un Settings de test sans hermes_url/hermes_api_key explicites — scripts/check.sh/scripts/_env.sh exportant tout le .env racine dans l'environnement du process pytest, ces champs héritaient silencieusement des vraies valeurs Hermes du .env local dès qu'elles y sont présentes (même avec _env_file=None, qui ne désactive que la lecture d'un fichier dotenv, pas os.environ) ; test_agent_stub échouait via scripts/check.sh (jamais via uv run pytest seul) tant que ce n'était pas corrigé. Corrigé en fixant explicitement hermes_url=""/hermes_api_key="" ; piège documenté dans apps/server/CLAUDE.md. Découvert en implémentant T-073 (T-073)

    Contract

    • v1 initial : docs/API.md, docs/DATA.md (T-001)
    • packages/contract/src/openapi.types.ts généré pour de vrai pour la première fois depuis packages/contract/openapi.json (scripts/gen-contract.sh) — remplace le placeholder Record<string, never> commité en T-001 et jamais régénéré depuis ; aucun changement de forme d'API (openapi.json et packages/contract/src/supabase.types.ts identiques bit à bit à la version précédente, aucune évolution de contrat depuis T-018) ; docs/API.md vérifié déjà aligné, aucune modification nécessaire (T-019)
    • AgentResult (/v1/agent) : ajout de actions: AgentAction[] ({ type, label?, payload }) et needs_confirmation: boolean, tous deux avec défaut vide/false — préparation du branchement Hermes (T-073). packages/contract/openapi.json/openapi.types.ts régénérés. Mergée après approbation humaine explicite (CLAUDE.md règle 1/3b), statut docs/STATUS.md corrigé en conséquence (T-072)
    • nouveau POST /v1/agent/stream (SSE, text/event-stream) — endpoint additif, /v1/agent inchangé. packages/contract/openapi.json/openapi.types.ts régénérés (supabase.types.ts non touché, aucun changement de schéma). Approbation humaine requise avant merge (CLAUDE.md règle 1/3b) (T-102)
    • nouveau GET /v1/prompts/base (BasePromptResult{base_style}) — endpoint additif, aucune route existante modifiée. packages/contract/openapi.json/openapi.types.ts régénérés (supabase.types.ts non touché, aucun changement de schéma Supabase). Approbation humaine requise avant merge (CLAUDE.md règle 1/3b) (T-101)
    Downloads