merge(workstate): intègre le snapshot délégations/file par agent (Lot B) dans develop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:52:34 +02:00
23 changed files with 1040 additions and 76 deletions

View File

@ -1,11 +1,9 @@
--- ---
upTo: 39f05602-a660-4632-accc-42d4887ce1ed upTo: b916af4a-6f94-4777-92b1-c5f89f898cde
objective: Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont objective: Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont
--- ---
**Objectif :** Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont **Objectif :** Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont
- **Response:** ✅ LS4 — Tests couche application « limites de session » : TOUT VERT, zéro régression. Commande : `cargo test -p application` Résultat : **tous les binaires de test verts, 0 failed** sur l'ensemble du crate. 16 nouveaux tests LS4 ajoutés, répartis en 2 fichiers d'intégration (style des tests applicatifs existants, 100 % fakes des ports). Confirmation explicite NON-RÉGRESSION : les suites préexistantes citées restent vertes — - `drain_with_readiness_lot1.rs` : **7/7** ✓ - `send_blocking_d1.rs` : **9/9** ✓ - (et tout le reste du crate : project/agent_lifecycle/orchestrator/structured_launch/etc., 0 échec). === Nouveau fichier `tests/session_limit_service.rs` (9 tests) — fakes Clock fixe / Scheduler enregistreur-contrôlable / EventBus espion / AgentResumer espion-contrôlable === (a) détection→planification : - `on_rate_limited_future_arms_and_emits_in_order` : Some(reset futur) ⇒ EXACTEMENT 1 `arm(fire_at_ms==reset, ResumeAgent{agent,node,conv})` + events `AgentRateLimited` PUIS `AgentResumeScheduled` dans cet ordre. - `on_rate_limited_past_reset_clamps_fire_at_to_now` : reset passé ⇒ `fire_at_ms==now` (clamp anti-passé) ; l'event RateLimited garde l'heure brute passée, ResumeScheduled porte le now clampé. - `on_rate_limited_without_reset_is_human_fallback_no_arm` : None ⇒ AUCUN arm + events `AgentRateLimited{None}` puis `AgentRateLimitSuspected{None}`. - `on_rate_limited_twice_same_agent_dedups_cancelling_previous` : 2 signaux même agent ⇒ l'ancien ScheduleId est cancel-é avant réarmement, et AUCUN `AgentResumeCancelled` émis (dédoublonnage interne silencieux, §21.10-4). (b) exécution : - `execute_resume_calls_resumer_with_prompt_and_emits_resumed` : resume(agent,node,conv, prompt==RESUME_PROMPT vérifié) + `AgentResumed` publié + entrée retirée (cancel_resume ultérieur ⇒ false). - `execute_resume_propagates_error_without_emitting_resumed` : Resumer Err ⇒ erreur propagée ET `AgentResumed` NON publié. (c) annulation : - `cancel_resume_after_arm_returns_true_and_emits_cancelled` : cancel renvoyant true ⇒ true + bon ScheduleId passé + `AgentResumeCancelled` publié. - `cancel_resume_without_arm_is_false_no_event` : sans armement ⇒ false, Scheduler non sollicité, aucun event. - `cancel_resume_when_scheduler_already_fired_is_false_no_event` : Scheduler renvoyant false (« déjà tiré ») ⇒ false ET PAS d'`AgentResumeCancelled` (contrat anti-course important). === Nouveau fichier `tests/session_limit_t4.rs` (7 tests) — réconciliation §21.2-T4, fakes AgentSession + InputMediator === - `outcome_rate_limited_some_without_final_is_graceful` : `[RateLimited{Some(t)}]``Ok(TurnOutcome::RateLimited{Some(t)})` (PAS d'Err) + vérifie qu'un RateLimited ne marque PAS Idle. - `outcome_rate_limited_none_without_final_is_graceful` : `[RateLimited{None}]``Ok(RateLimited{None})`. - `outcome_rate_limited_then_final_is_completed` : `[.., RateLimited, Final]``Completed(contenu)` (Final l'emporte) + Idle marqué. - `outcome_truncated_stream_without_final_or_ratelimit_is_io_error` : `[TextDelta]` seul ⇒ `Err(Io)` INCHANGÉ (non-régression critique du flux tronqué). - non-régression signatures historiques : `drain_with_readiness_rate_limited_is_io_error`, `send_blocking_rate_limited_is_io_error` (limite ⇒ Io), `drain_with_readiness_nominal_still_completes` (cas nominal Final intact). Aucun bug détecté : le code DevBackend est conforme au cadrage §21.5 sur tous les axes (ordre des events, clamp via plan_resume, dédoublonnage silencieux, prompt constant, propagation d'erreur sans event, contrat anti-course du cancel). Je n'ai modifié AUCUNE ligne de prod (uniquement ajouté 2 fichiers de tests). Feature LS4 validée côté tests application. Prêt pour la suite du cycle (Git : merge éventuel).
- **Prompt:** Tests LS5 — parser regex niveau 2 (infra) : `RateLimitParser` (crates/infrastructure/src/ratelimit/mod.rs) + le module d'heure partagé `timeparse` (crates/infrastructure/src/timeparse.rs). Branche feature/agent-session-limits. DevBackend a livré (cargo build -p infrastructure OK, 0 warning ; tests LS2 toujours verts). Tout est pur (now_ms injecté), testable sans I/O. Écris et exécute les tests dans le style existant. RateLimitParser (new + detect + applies) : - `new` sur regex INVALIDE → None (jamais de panique) ; - pattern qui matche SANS reset_capture → `Some(SessionLimit{ resets_at_ms: None, source: Pattern, detected_at_ms == now_ms })` ; - capture nommée `(?P<reset>...)` + time_format ABSOLU : `epoch_s` (secondes→×1000), `epoch_ms` (tel quel), `iso8601`/`rfc3339` (`...Z` → ms attendus) → resets_at_ms corrects ; - time_format RELATIF (`relative_s`, capture « 600 », now=T) → `Some(resets_at_ms == T + 600_000)` ; - time_format MURAL (`wall`, capture « 3pm ») : now correspondant à 10h du jour → 15h AUJOURD'HUI (même jour UTC) ; now correspondant à 16h → 15h DEMAIN (passage de minuit, +24h). Choisis des now_ms calculés proprement (epoch connu) et calcule l'attendu à la main ; - pattern NE matche PAS → detect → None ; - pattern matche mais capture absente/valeur pourrie/non parsable → `Some(SessionLimit{ resets_at_ms: None })` (détection utile sans heure) ; - vérifie que `source == RateLimitSource::Pattern` dans tous les cas détectés ; - compilation du regex faite une seule fois (à new) — au minimum vérifie que detect peut être appelé plusieurs fois sans souci. applies(profile) : - profil avec structured_adapter (structuré) → false (même s'il a un rate_limit_pattern) ; - profil PTY (sans structured_adapter) AVEC rate_limit_pattern → true ; - profil PTY SANS rate_limit_pattern → false. timeparse (fonctions réexportées) : - `days_from_civil` : 1970-01-01 → 0 ; une année bissextile (2024-02-29) cohérente ; - `parse_wall_clock` : « 3pm » → (15,0,0) ; « 15:00:30 » → (15,0,30) ; format invalide → None ; - `wall_clock_to_ms` : même jour vs lendemain selon now ; - `parse_absolute_ms` : epoch s/ms/ISO cohérents (recoupe avec LS2 si utile). - NON-RÉGRESSION : confirme que les tests LS2 (parse_reset_ms/parse_event dans session/mod.rs) restent verts après le refactor qui a déplacé les helpers vers timeparse. Exécute `cargo test -p infrastructure`. Rends : rapport complet (nb tests, pass/fail), confirmation explicite que LS2/LS3 restent verts, et en cas d'échec rapport CLAIR (attendu vs obtenu) pour DevBackend. EN PLUS (petit nettoyage que DevBackend t'a signalé) : tes tests scheduler LS3 (crates/infrastructure/src/scheduler/mod.rs, ~lignes 253/285) déclenchent 2 warnings `irrefutable if let` car ScheduledTask est mono-variante aujourd'hui. Corrige ces 2 warnings dans le code de TEST (ex. déstructuration directe au lieu de `if let`) pour garder la suite sans warning — c'est du test, tu peux le modifier. Ne touche à aucun code de prod ; signale tout vrai bug sans le corriger.
- **Response:** ✅ LS5 — Tests parser regex niveau 2 (`RateLimitParser`) + module `timeparse` : TOUT VERT, 0 warning, zéro régression. Commande : `cargo test -p infrastructure` Résultat lib : **221 passed; 0 failed; 0 ignored** (195 antérieurs + **26 nouveaux LS5**). Tous les fichiers d'intégration + doctests verts. **Build de test : 0 warning** (vérifié `--no-run | grep -c warning` = 0). Confirmation explicite NON-RÉGRESSION : - **LS2** (`session::tests::parse_reset_ms*` / `parse_event*` / `send_emits_rate*`) : tous verts APRÈS le refactor qui a déplacé les helpers vers `timeparse` ✓ - **LS3** (`scheduler::tests::*`) : 7/7 verts ✓ (+ les 2 warnings `irrefutable if let` corrigés — voir plus bas) === `ratelimit/mod.rs` — 15 tests (in-file `#[cfg(test)] mod tests`) === new + detect : - `new_returns_none_on_invalid_regex` : regex invalide `"rate limit ("` ⇒ None (jamais de panique). - `detect_returns_none_when_pattern_does_not_match` : pas de match ⇒ None. - `detect_match_without_reset_capture_has_no_time` : match sans reset_capture ⇒ `SessionLimit{resets_at_ms:None, source:Pattern, detected_at_ms==now}`. - formats ABSOLUS : `detect_epoch_seconds_format` (×1000), `detect_epoch_millis_format` (tel quel), `detect_iso8601_format` (`2023-11-14T22:13:20Z`→1_700_000_000_000). - format RELATIF : `detect_relative_seconds_format_uses_now` (capture « 600 », now=T ⇒ T+600_000). - format MURAL (passage de minuit, math calculée à la main sur DAY_START=1_699_920_000_000 = 2023-11-14T00:00Z) : `detect_wall_clock_same_day_when_future` (now=10h, « 3pm » ⇒ 15h même jour) ; `detect_wall_clock_next_day_when_past` (now=16h ⇒ 15h DEMAIN, +24h). - capture inexploitable ⇒ détection sans heure : `detect_match_with_missing_capture_group_has_no_time`, `detect_match_with_unparsable_value_has_no_time` (⇒ `resets_at_ms:None`). - `detect_can_be_called_multiple_times` : regex compilé une seule fois, detect appelé 3× sans souci. - `source == RateLimitSource::Pattern` asserté dans tous les cas détectés. applies(profile) : - `applies_false_for_structured_profile_even_with_pattern` (structuré ⇒ false, niveau 1 prend la main) ; - `applies_true_for_pty_profile_with_pattern` (PTY + pattern ⇒ true) ; - `applies_false_for_pty_profile_without_pattern` (PTY sans pattern ⇒ false). === `timeparse.rs` — 11 tests === - `days_from_civil_epoch_is_zero` (1970-01-01→0) ; `days_from_civil_leap_day_2024` (2024-02-29→19782, + cohérence avec le 1ᵉʳ mars). - `parse_wall_clock_variants` (« 3pm »→(15,0,0), « 15:00:30 »→(15,0,30), « 3:30 pm »→(15,30,0), 12am→0h, 12pm→12h) ; `parse_wall_clock_rejects_invalid` (texte, 25:00, 10:75, trop de composantes ⇒ None). - `wall_clock_to_ms_*` : même jour / lendemain / cas-limite « cible == now » (⇒ lendemain, jamais le présent). - `parse_absolute_ms_*` : epoch s/ms, ISO8601, garbage→None (recoupe LS2). - `parse_rfc3339_offsets_convert_to_utc` : +01:00 et 01:00 ⇒ même instant UTC. === Nettoyage LS3 demandé === Les 2 warnings `irrefutable if let` (scheduler/mod.rs ~253/285, `ScheduledTask` mono-variante) sont corrigés : remplacés par une **déstructuration directe** `let ScheduledTask::ResumeAgent { conversation_id, .. } = task;` (code de TEST uniquement). Suite désormais 0 warning. Aucun bug détecté : le code DevBackend est conforme au cadrage §21 niveau 2 sur tous les axes (robustesse regex, stratégies de format absolu/relatif/mural, passage de minuit UTC, règle anti-double-détection `applies`, factorisation `timeparse` sans casser LS2). Je n'ai modifié AUCUN code de prod (uniquement ajouté des tests + corrigé mes propres tests LS3). Feature LS5 validée côté tests infra. Prêt pour la suite du cycle. - **Response:** ✅ LS5 — Tests parser regex niveau 2 (`RateLimitParser`) + module `timeparse` : TOUT VERT, 0 warning, zéro régression. Commande : `cargo test -p infrastructure` Résultat lib : **221 passed; 0 failed; 0 ignored** (195 antérieurs + **26 nouveaux LS5**). Tous les fichiers d'intégration + doctests verts. **Build de test : 0 warning** (vérifié `--no-run | grep -c warning` = 0). Confirmation explicite NON-RÉGRESSION : - **LS2** (`session::tests::parse_reset_ms*` / `parse_event*` / `send_emits_rate*`) : tous verts APRÈS le refactor qui a déplacé les helpers vers `timeparse` ✓ - **LS3** (`scheduler::tests::*`) : 7/7 verts ✓ (+ les 2 warnings `irrefutable if let` corrigés — voir plus bas) === `ratelimit/mod.rs` — 15 tests (in-file `#[cfg(test)] mod tests`) === new + detect : - `new_returns_none_on_invalid_regex` : regex invalide `"rate limit ("` ⇒ None (jamais de panique). - `detect_returns_none_when_pattern_does_not_match` : pas de match ⇒ None. - `detect_match_without_reset_capture_has_no_time` : match sans reset_capture ⇒ `SessionLimit{resets_at_ms:None, source:Pattern, detected_at_ms==now}`. - formats ABSOLUS : `detect_epoch_seconds_format` (×1000), `detect_epoch_millis_format` (tel quel), `detect_iso8601_format` (`2023-11-14T22:13:20Z`→1_700_000_000_000). - format RELATIF : `detect_relative_seconds_format_uses_now` (capture « 600 », now=T ⇒ T+600_000). - format MURAL (passage de minuit, math calculée à la main sur DAY_START=1_699_920_000_000 = 2023-11-14T00:00Z) : `detect_wall_clock_same_day_when_future` (now=10h, « 3pm » ⇒ 15h même jour) ; `detect_wall_clock_next_day_when_past` (now=16h ⇒ 15h DEMAIN, +24h). - capture inexploitable ⇒ détection sans heure : `detect_match_with_missing_capture_group_has_no_time`, `detect_match_with_unparsable_value_has_no_time` (⇒ `resets_at_ms:None`). - `detect_can_be_called_multiple_times` : regex compilé une seule fois, detect appelé 3× sans souci. - `source == RateLimitSource::Pattern` asserté dans tous les cas détectés. applies(profile) : - `applies_false_for_structured_profile_even_with_pattern` (structuré ⇒ false, niveau 1 prend la main) ; - `applies_true_for_pty_profile_with_pattern` (PTY + pattern ⇒ true) ; - `applies_false_for_pty_profile_without_pattern` (PTY sans pattern ⇒ false). === `timeparse.rs` — 11 tests === - `days_from_civil_epoch_is_zero` (1970-01-01→0) ; `days_from_civil_leap_day_2024` (2024-02-29→19782, + cohérence avec le 1ᵉʳ mars). - `parse_wall_clock_variants` (« 3pm »→(15,0,0), « 15:00:30 »→(15,0,30), « 3:30 pm »→(15,30,0), 12am→0h, 12pm→12h) ; `parse_wall_clock_rejects_invalid` (texte, 25:00, 10:75, trop de composantes ⇒ None). - `wall_clock_to_ms_*` : même jour / lendemain / cas-limite « cible == now » (⇒ lendemain, jamais le présent). - `parse_absolute_ms_*` : epoch s/ms, ISO8601, garbage→None (recoupe LS2). - `parse_rfc3339_offsets_convert_to_utc` : +01:00 et 01:00 ⇒ même instant UTC. === Nettoyage LS3 demandé === Les 2 warnings `irrefutable if let` (scheduler/mod.rs ~253/285, `ScheduledTask` mono-variante) sont corrigés : remplacés par une **déstructuration directe** `let ScheduledTask::ResumeAgent { conversation_id, .. } = task;` (code de TEST uniquement). Suite désormais 0 warning. Aucun bug détecté : le code DevBackend est conforme au cadrage §21 niveau 2 sur tous les axes (robustesse regex, stratégies de format absolu/relatif/mural, passage de minuit UTC, règle anti-double-détection `applies`, factorisation `timeparse` sans casser LS2). Je n'ai modifié AUCUN code de prod (uniquement ajouté des tests + corrigé mes propres tests LS3). Feature LS5 validée côté tests infra. Prêt pour la suite du cycle.
- **Prompt:** Tâche QA — tests unitaires de la slice LS7-front (UI limites de session des agents), branche `feature/agent-session-limits`. DevFrontend vient d'implémenter (typecheck `tsc --noEmit` propre, 39 tests agents existants toujours verts, non commité). Périmètre à couvrir (frontend/) : 1. `src/features/agents/useAgents.ts` — nouvel état `limitByAgent: Record<string, AgentLimitState>` (`{ limitedUntil?, resumeFireAt?, suspected? }`) peuplé depuis 5 DOMAIN_EVENT dans l'abonnement `onDomainEvent`. À tester (via le mock gateway qui émet des events) : - `agentRateLimited` {agentId, resetsAtMs} → entrée `{ limitedUntil: resetsAtMs, suspected: false }`. - `agentResumeScheduled` {agentId, fireAtMs} → arme `resumeFireAt`. - `agentResumeCancelled` {agentId} → retire `resumeFireAt`, reste limité. - `agentResumed` {agentId} → supprime l'entrée (état effacé). - `agentRateLimitSuspected` {agentId, resetsAtMs?} → `{ ..., suspected: true }`, y compris le cas SANS `resetsAtMs` (heure inconnue). - séquence réaliste : rateLimited → resumeScheduled → cancelResume (action) → vérifier retrait optimiste + appel `input.cancelResume` (mock `cancelledResumes` / `cancelResumeResult`). - Action `cancelResume(agentId)` exposée par le hook : retrait optimiste + verdict backend renvoyé (teste les deux verdicts via `cancelResumeResult`). 2. `src/features/agents/AgentLimitBadge.tsx` — helpers purs exportés `formatResetTime(epochMs)` (→ HH:MM) et `formatCountdown(remainingMs)` (→ « Xm Ys »/« Ys », clampé à 0 pour négatif). Teste ces helpers purs directement (cas limites : 0, négatif, < 60 s, > 60 s, minuit). Teste le rendu du badge (limité jusqu'à HH:MM vs « limité » sans heure vs « heure inconnue » pour suspected sans resetsAtMs) et que le bouton « Annuler la reprise » appelle l'action quand `resumeFireAt` est armé. 3. Adapter mock `src/adapters/mock/index.ts``MockInputGateway.cancelResume` (`cancelledResumes`, `cancelResumeResult`) : couvre via les tests du hook. Suis le style des tests existants (cf. `singletonAgent.test.tsx`, `mock.test.ts`, les tests agents). Exécute la suite (`npm test` ou l'outil du repo) et RENDS-MOI via idea_reply : la commande exacte lancée, le résultat réel (nb tests passés/échoués), et le rapport d'erreurs complet si rouge. Si un test révèle un bug d'implémentation, décris-le précisément (fichier:ligne, attendu vs obtenu) — je relaierai à DevFrontend pour correction. Ne commit rien. - **Prompt:** Tâche QA — tests unitaires de la slice LS7-front (UI limites de session des agents), branche `feature/agent-session-limits`. DevFrontend vient d'implémenter (typecheck `tsc --noEmit` propre, 39 tests agents existants toujours verts, non commité). Périmètre à couvrir (frontend/) : 1. `src/features/agents/useAgents.ts` — nouvel état `limitByAgent: Record<string, AgentLimitState>` (`{ limitedUntil?, resumeFireAt?, suspected? }`) peuplé depuis 5 DOMAIN_EVENT dans l'abonnement `onDomainEvent`. À tester (via le mock gateway qui émet des events) : - `agentRateLimited` {agentId, resetsAtMs} → entrée `{ limitedUntil: resetsAtMs, suspected: false }`. - `agentResumeScheduled` {agentId, fireAtMs} → arme `resumeFireAt`. - `agentResumeCancelled` {agentId} → retire `resumeFireAt`, reste limité. - `agentResumed` {agentId} → supprime l'entrée (état effacé). - `agentRateLimitSuspected` {agentId, resetsAtMs?} → `{ ..., suspected: true }`, y compris le cas SANS `resetsAtMs` (heure inconnue). - séquence réaliste : rateLimited → resumeScheduled → cancelResume (action) → vérifier retrait optimiste + appel `input.cancelResume` (mock `cancelledResumes` / `cancelResumeResult`). - Action `cancelResume(agentId)` exposée par le hook : retrait optimiste + verdict backend renvoyé (teste les deux verdicts via `cancelResumeResult`). 2. `src/features/agents/AgentLimitBadge.tsx` — helpers purs exportés `formatResetTime(epochMs)` (→ HH:MM) et `formatCountdown(remainingMs)` (→ « Xm Ys »/« Ys », clampé à 0 pour négatif). Teste ces helpers purs directement (cas limites : 0, négatif, < 60 s, > 60 s, minuit). Teste le rendu du badge (limité jusqu'à HH:MM vs « limité » sans heure vs « heure inconnue » pour suspected sans resetsAtMs) et que le bouton « Annuler la reprise » appelle l'action quand `resumeFireAt` est armé. 3. Adapter mock `src/adapters/mock/index.ts``MockInputGateway.cancelResume` (`cancelledResumes`, `cancelResumeResult`) : couvre via les tests du hook. Suis le style des tests existants (cf. `singletonAgent.test.tsx`, `mock.test.ts`, les tests agents). Exécute la suite (`npm test` ou l'outil du repo) et RENDS-MOI via idea_reply : la commande exacte lancée, le résultat réel (nb tests passés/échoués), et le rapport d'erreurs complet si rouge. Si un test révèle un bug d'implémentation, décris-le précisément (fichier:ligne, attendu vs obtenu) — je relaierai à DevFrontend pour correction. Ne commit rien.
- **Response:** QA LS7-front — VERT. Aucun bug d'implémentation trouvé. Rien commité. ## Fichiers de test ajoutés (2) - `frontend/src/features/agents/useAgentsLimits.test.tsx` (13 tests) — état `limitByAgent` du hook via le `MockSystemGateway` qui émet les events + `MockInputGateway`. - `frontend/src/features/agents/AgentLimitBadge.test.tsx` (11 tests) — helpers purs + rendu du badge. ## Commandes exactes lancées (cwd = frontend/) 1. `npx vitest run src/features/agents/useAgentsLimits.test.tsx src/features/agents/AgentLimitBadge.test.tsx`**Test Files 2 passed (2) · Tests 24 passed (24)** 2. `npx vitest run src/features/agents/` (suite agents complète, régression) → **Test Files 4 passed (4) · Tests 63 passed (63)** (39 existants + 24 nouveaux, 0 régression) ## Couverture livrée useAgents (§21), tous via events réels : - `agentRateLimited{resetsAtMs}``{limitedUntil, resumeFireAt:undefined, suspected:false}` ✓ - `agentResumeScheduled{fireAtMs}` → arme `resumeFireAt` par-dessus l'état limité ✓ - `agentResumeCancelled` → retire `resumeFireAt`, reste limité ✓ ; + no-op sur agent inconnu (aucune entrée créée) ✓ - `agentResumed` → entrée supprimée (`agentId in map === false`) ✓ - `agentRateLimitSuspected` AVEC et SANS `resetsAtMs``suspected:true`, `limitedUntil` undefined dans le cas sans heure ✓ - séquence réaliste rateLimited→scheduled→`cancelResume(action)` : retrait optimiste du countdown + agent toujours limité + `input.cancelledResumes === [agentId]` + verdict ✓ - action `cancelResume` : verdict `true` ET `false` (via `cancelResumeResult`), + appel du port même sans reprise armée ✓ - deux agents foldés indépendamment ✓ AgentLimitBadge : - `formatCountdown` : 0→"0s", négatif→"0s" (clamp), <60s→"Xs", ceil partiel (4.2s→"5s"), 60s→"Xm Ys" (60000→"1m 0s", 125000→"2m 5s") - `formatResetTime` : HH:MM sans secondes (même minute +30s = même libellé), minuit stable - rendu : "limité jusqu'à HH:MM" (heure connue) / "limité" (sans heure) / note "heure inconnue reprise à préciser" (suspected sans heure) + absence de la note quand suspected A une heure - bouton "Annuler la reprise" : appelle `onCancelResume` au clic quand `resumeFireAt` armé, countdown rendu, bouton désactivé si `busy` MockInputGateway.cancelResume (`cancelledResumes`/`cancelResumeResult`) : couvert via les tests du hook. ## Note hors-périmètre (PRÉEXISTANTE, pas causée par LS7-front) `src/adapters/mock/mock.test.ts:16` échoue (1 test) : il assert « thirteen gateways » mais le `permission` gateway porte le compte à 14. Vérifié : la gateway `permission` existe déjà dans HEAD committé (eca2ba9) l'assertion de comptage est simplement périmée, sans rapport avec les limites de session. Mes deux fichiers ne touchent pas ce test. Aucune action de ma part (hors scope) ; à relayer pour une mise à jour du compteur si tu veux la suite `adapters/mock` 100% verte. - **Response:** QA LS7-front — VERT. Aucun bug d'implémentation trouvé. Rien commité. ## Fichiers de test ajoutés (2) - `frontend/src/features/agents/useAgentsLimits.test.tsx` (13 tests) — état `limitByAgent` du hook via le `MockSystemGateway` qui émet les events + `MockInputGateway`. - `frontend/src/features/agents/AgentLimitBadge.test.tsx` (11 tests) — helpers purs + rendu du badge. ## Commandes exactes lancées (cwd = frontend/) 1. `npx vitest run src/features/agents/useAgentsLimits.test.tsx src/features/agents/AgentLimitBadge.test.tsx`**Test Files 2 passed (2) · Tests 24 passed (24)** 2. `npx vitest run src/features/agents/` (suite agents complète, régression) → **Test Files 4 passed (4) · Tests 63 passed (63)** (39 existants + 24 nouveaux, 0 régression) ## Couverture livrée useAgents (§21), tous via events réels : - `agentRateLimited{resetsAtMs}``{limitedUntil, resumeFireAt:undefined, suspected:false}` ✓ - `agentResumeScheduled{fireAtMs}` → arme `resumeFireAt` par-dessus l'état limité ✓ - `agentResumeCancelled` → retire `resumeFireAt`, reste limité ✓ ; + no-op sur agent inconnu (aucune entrée créée) ✓ - `agentResumed` → entrée supprimée (`agentId in map === false`) ✓ - `agentRateLimitSuspected` AVEC et SANS `resetsAtMs``suspected:true`, `limitedUntil` undefined dans le cas sans heure ✓ - séquence réaliste rateLimited→scheduled→`cancelResume(action)` : retrait optimiste du countdown + agent toujours limité + `input.cancelledResumes === [agentId]` + verdict ✓ - action `cancelResume` : verdict `true` ET `false` (via `cancelResumeResult`), + appel du port même sans reprise armée ✓ - deux agents foldés indépendamment ✓ AgentLimitBadge : - `formatCountdown` : 0→"0s", négatif→"0s" (clamp), <60s→"Xs", ceil partiel (4.2s→"5s"), 60s→"Xm Ys" (60000→"1m 0s", 125000→"2m 5s") - `formatResetTime` : HH:MM sans secondes (même minute +30s = même libellé), minuit stable - rendu : "limité jusqu'à HH:MM" (heure connue) / "limité" (sans heure) / note "heure inconnue reprise à préciser" (suspected sans heure) + absence de la note quand suspected A une heure - bouton "Annuler la reprise" : appelle `onCancelResume` au clic quand `resumeFireAt` armé, countdown rendu, bouton désactivé si `busy` MockInputGateway.cancelResume (`cancelledResumes`/`cancelResumeResult`) : couvert via les tests du hook. ## Note hors-périmètre (PRÉEXISTANTE, pas causée par LS7-front) `src/adapters/mock/mock.test.ts:16` échoue (1 test) : il assert « thirteen gateways » mais le `permission` gateway porte le compte à 14. Vérifié : la gateway `permission` existe déjà dans HEAD committé (eca2ba9) l'assertion de comptage est simplement périmée, sans rapport avec les limites de session. Mes deux fichiers ne touchent pas ce test. Aucune action de ma part (hors scope) ; à relayer pour une mise à jour du compteur si tu veux la suite `adapters/mock` 100% verte.
@ -24,3 +22,5 @@ objective: Tâche : ajouter UN test fonctionnel anti-régression de la communica
- **Response:** Verdict QA final : vert accepté avec réserve environnementale. Je ne maintiens pas le KO global, car le rouge restant est maintenant expliqué et borné : dans cet environnement sandbox, un bind de socket Unix réel échoue `PermissionDenied` même pour un smoke minimal `UnixListener::bind` dans `/tmp` et `/run/user/1000`. Les tests qui exigent un vrai socket Unix loopback ne peuvent donc pas être probants ici. Le reste du périmètre applicatif, hotfix submit/logs, frontend, wiring hors vrai socket et app-tauri lib hors tests loopback réels est vert. Commandes vertes déjà validées QA sur ce périmètre : ```text cargo test -p application compose_convention_file --no-fail-fast ``` Résultat : `15 passed; 0 failed; 28 filtered out`. ```text cargo test -p application ``` Résultat : `43 passed; 0 failed` pour les unit tests application, tous les tests dintégration application et doctests passés. Warning existant : `method writes is never used` dans `crates/application/tests/orchestrator_service.rs:1642`. ```text cargo test -p infrastructure input ``` Résultat : `35 passed; 0 failed; 187 filtered out`. ```text cargo test -p application --test orchestrator_service ``` Résultat : `45 passed; 0 failed` avec le même warning existant `writes`. ```text cd frontend && npx vitest run src/features/terminals/useWritePortal.test.tsx ``` Résultat : `1 passed`, `12 tests passed`. Warnings Vite existants sur options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx vitest run src/features/terminals/TerminalView.portal.test.tsx src/features/terminals/TerminalView.test.tsx src/features/terminals/useWritePortal.test.tsx ``` Résultat : `3 passed`, `24 tests passed`. Même warnings Vite. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. ```text cargo check -p app-tauri ``` Résultat : succès, `Finished dev profile`. Commandes de confirmation après diagnostic sandbox, relancées par QA : ```text cargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together ``` Résultat réel : ```text running 9 tests ... test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat réel : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Réserve exacte : non-validé dans ce sandbox uniquement pour les tests nécessitant la création/binding dun vrai socket Unix loopback : - `crates/app-tauri/tests/orchestrator_wiring.rs` : `open_binds_the_project_loopback_endpoint`, `double_open_keeps_a_single_endpoint_no_address_in_use`, `close_cleans_up_the_endpoint_socket_file`, `file_watcher_and_loopback_endpoint_live_together`. - `app-tauri --lib` : `state::mcp_e2e_loopback_tests`, `state::bind_endpoint_d1_tests`, `mcp_bridge::tests::end_to_end_over_real_loopback`. Ces tests restent à exécuter dans un environnement non sandboxé permettant `UnixListener::bind` sur socket filesystem. Je nai modifié aucun fichier. - **Response:** Verdict QA final : vert accepté avec réserve environnementale. Je ne maintiens pas le KO global, car le rouge restant est maintenant expliqué et borné : dans cet environnement sandbox, un bind de socket Unix réel échoue `PermissionDenied` même pour un smoke minimal `UnixListener::bind` dans `/tmp` et `/run/user/1000`. Les tests qui exigent un vrai socket Unix loopback ne peuvent donc pas être probants ici. Le reste du périmètre applicatif, hotfix submit/logs, frontend, wiring hors vrai socket et app-tauri lib hors tests loopback réels est vert. Commandes vertes déjà validées QA sur ce périmètre : ```text cargo test -p application compose_convention_file --no-fail-fast ``` Résultat : `15 passed; 0 failed; 28 filtered out`. ```text cargo test -p application ``` Résultat : `43 passed; 0 failed` pour les unit tests application, tous les tests dintégration application et doctests passés. Warning existant : `method writes is never used` dans `crates/application/tests/orchestrator_service.rs:1642`. ```text cargo test -p infrastructure input ``` Résultat : `35 passed; 0 failed; 187 filtered out`. ```text cargo test -p application --test orchestrator_service ``` Résultat : `45 passed; 0 failed` avec le même warning existant `writes`. ```text cd frontend && npx vitest run src/features/terminals/useWritePortal.test.tsx ``` Résultat : `1 passed`, `12 tests passed`. Warnings Vite existants sur options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx vitest run src/features/terminals/TerminalView.portal.test.tsx src/features/terminals/TerminalView.test.tsx src/features/terminals/useWritePortal.test.tsx ``` Résultat : `3 passed`, `24 tests passed`. Même warnings Vite. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. ```text cargo check -p app-tauri ``` Résultat : succès, `Finished dev profile`. Commandes de confirmation après diagnostic sandbox, relancées par QA : ```text cargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together ``` Résultat réel : ```text running 9 tests ... test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat réel : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Réserve exacte : non-validé dans ce sandbox uniquement pour les tests nécessitant la création/binding dun vrai socket Unix loopback : - `crates/app-tauri/tests/orchestrator_wiring.rs` : `open_binds_the_project_loopback_endpoint`, `double_open_keeps_a_single_endpoint_no_address_in_use`, `close_cleans_up_the_endpoint_socket_file`, `file_watcher_and_loopback_endpoint_live_together`. - `app-tauri --lib` : `state::mcp_e2e_loopback_tests`, `state::bind_endpoint_d1_tests`, `mcp_bridge::tests::end_to_end_over_real_loopback`. Ces tests restent à exécuter dans un environnement non sandboxé permettant `UnixListener::bind` sur socket filesystem. Je nai modifié aucun fichier.
- **Prompt:** QA globale pour le chantier `feature/conversation-ux-live-state` Lot A. Périmètre implémenté: backend/Tauri read-model `get_project_work_state(projectId)` + frontend onglet/panneau Work read-only. Backend a ajouté `crates/application/src/workstate/mod.rs`, snapshots live, commande/DTO Tauri; frontend a ajouté types/domain/port/adapter/mock/hook/panel et onglet Work. Merci de relire le périmètre: read-only, pas nouvelle persistance, pas lecture log/handoff, pas FIFO complète, ordre manifeste, live agents hors manifeste ignorés. Exécute les tests pertinents réels. Minimum: - `cargo test -p application workstate` - `cargo test -p application` - `cargo check -p app-tauri` - tests app-tauri DTO/live pertinents (`dto_agents`, `list_live_agents_r0b`) et si nécessaire app-tauri lib avec skips socket connus - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Retourne verdict, commandes exactes/résultats, risques, et si les tests socket Unix restent filtrés pour raison sandbox déjà diagnostiquée. Ne modifie pas le code sauf correction de test strictement nécessaire. - **Prompt:** QA globale pour le chantier `feature/conversation-ux-live-state` Lot A. Périmètre implémenté: backend/Tauri read-model `get_project_work_state(projectId)` + frontend onglet/panneau Work read-only. Backend a ajouté `crates/application/src/workstate/mod.rs`, snapshots live, commande/DTO Tauri; frontend a ajouté types/domain/port/adapter/mock/hook/panel et onglet Work. Merci de relire le périmètre: read-only, pas nouvelle persistance, pas lecture log/handoff, pas FIFO complète, ordre manifeste, live agents hors manifeste ignorés. Exécute les tests pertinents réels. Minimum: - `cargo test -p application workstate` - `cargo test -p application` - `cargo check -p app-tauri` - tests app-tauri DTO/live pertinents (`dto_agents`, `list_live_agents_r0b`) et si nécessaire app-tauri lib avec skips socket connus - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Retourne verdict, commandes exactes/résultats, risques, et si les tests socket Unix restent filtrés pour raison sandbox déjà diagnostiquée. Ne modifie pas le code sauf correction de test strictement nécessaire.
- **Response:** Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels. Périmètre relu : - Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`. - Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète. - Ordre manifeste conservé par itération sur `manifest.entries`. - Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis. - Snapshot live typé ajouté sans changer `live_agents()` existant. - Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`. - Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants. Commandes exécutées et résultats : ```text cargo fmt -p application -p app-tauri -- --check ``` Résultat : OK, aucune sortie. ```text cargo test -p application workstate ``` Résultat : ```text running 5 tests test workstate_attaches_live_pty_session_to_manifest_agent ... ok test workstate_includes_busy_state_from_input_mediator ... ok test workstate_attaches_live_structured_session_to_manifest_agent ... ok test workstate_ignores_live_agents_absent_from_manifest ... ok test workstate_lists_manifest_agents_idle_without_live_sessions ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Warning existant observé : ```text warning: method `writes` is never used --> crates/application/tests/orchestrator_service.rs:1642:8 ``` ```text cargo test -p application ``` Résultat : ```text running 43 tests ... test result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ... Running tests/workstate.rs running 5 tests ... test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests application running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Même warning existant `CapturingFs::writes` unused. ```text cargo check -p app-tauri ``` Résultat : ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s ``` ```text cargo test -p app-tauri --test dto_agents ``` Résultat : ```text running 19 tests ... test project_work_state_dto_serialises_live_and_busy_camelcase ... ok ... test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --test list_live_agents_r0b ``` Résultat : ```text running 5 tests test no_sessions_yields_empty_list ... ok test both_kinds_live_listed_without_duplicates ... ok test pty_live_agent_is_listed ... ok test same_agent_in_both_registries_is_deduplicated ... ok test structured_live_agent_is_listed ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Les 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A. ```text cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` Résultat : ```text Test Files 2 passed (2) Tests 13 passed (13) ``` Warnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. Risques restants : - Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas dhistorique conversationnel, pas de lecture log/handoff. - Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem. - Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot. Aucune correction requise. Je nai modifié aucun fichier. - **Response:** Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels. Périmètre relu : - Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`. - Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète. - Ordre manifeste conservé par itération sur `manifest.entries`. - Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis. - Snapshot live typé ajouté sans changer `live_agents()` existant. - Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`. - Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants. Commandes exécutées et résultats : ```text cargo fmt -p application -p app-tauri -- --check ``` Résultat : OK, aucune sortie. ```text cargo test -p application workstate ``` Résultat : ```text running 5 tests test workstate_attaches_live_pty_session_to_manifest_agent ... ok test workstate_includes_busy_state_from_input_mediator ... ok test workstate_attaches_live_structured_session_to_manifest_agent ... ok test workstate_ignores_live_agents_absent_from_manifest ... ok test workstate_lists_manifest_agents_idle_without_live_sessions ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Warning existant observé : ```text warning: method `writes` is never used --> crates/application/tests/orchestrator_service.rs:1642:8 ``` ```text cargo test -p application ``` Résultat : ```text running 43 tests ... test result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ... Running tests/workstate.rs running 5 tests ... test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests application running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Même warning existant `CapturingFs::writes` unused. ```text cargo check -p app-tauri ``` Résultat : ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s ``` ```text cargo test -p app-tauri --test dto_agents ``` Résultat : ```text running 19 tests ... test project_work_state_dto_serialises_live_and_busy_camelcase ... ok ... test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --test list_live_agents_r0b ``` Résultat : ```text running 5 tests test no_sessions_yields_empty_list ... ok test both_kinds_live_listed_without_duplicates ... ok test pty_live_agent_is_listed ... ok test same_agent_in_both_registries_is_deduplicated ... ok test structured_live_agent_is_listed ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Les 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A. ```text cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` Résultat : ```text Test Files 2 passed (2) Tests 13 passed (13) ``` Warnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. Risques restants : - Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas dhistorique conversationnel, pas de lecture log/handoff. - Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem. - Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot. Aucune correction requise. Je nai modifié aucun fichier.
- **Prompt:** Valide le Lot B complet `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`. Périmètre à vérifier: - Backend/Tauri: port read-only `AgentQueueSnapshot`, snapshots FIFO d'`InMemoryMailbox`, `GetProjectWorkState.agents[].tickets`, DTO camelCase, wiring `AppState`. - Frontend: types `tickets`, mock normalisé, panneau Work affiche tickets FIFO inProgress/queued, refresh sur `delegationReady`. - Les tickets human et agent sont inclus; l'UI ne doit pas les présenter tous comme délégations agent. Commandes minimales demandées (adapte si le repo impose un cwd): - `cargo fmt --all -- --check` - `cargo test -p infrastructure mailbox --lib` - `cargo test -p application --test workstate` - `cargo test -p app-tauri --test dto_agents` - `cargo check -p app-tauri` - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Tu peux ajouter des commandes pertinentes, mais ne committe pas et ne modifie pas les fichiers applicatifs sauf tests QA indispensables. Ignore le dirty runtime `.ideai/*` s'il est seulement live-session. Retour attendu: verdict clair, commandes exécutées avec résultats réels, anomalies éventuelles avec fichiers/lignes, et si rouge, donne la sortie d'erreur exploitable.
- **Response:** Verdict: VERT pour le Lot B `workstate delegation/queue snapshot` sur `feature/workstate-delegation-queue`. Je nai modifié aucun fichier. Dirty ignoré côté runtime `.ideai/*`; les fichiers applicatifs/test du lot étaient déjà modifiés dans le worktree. Commandes exécutées et résultats réels: 1. `cargo fmt --all -- --check` - Exit 0, aucune sortie. 2. `cargo test -p infrastructure mailbox --lib` - Exit 0. - `running 13 tests` - `test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 215 filtered out; finished in 0.00s` - Couvre notamment `snapshot_preserves_fifo_order_and_positions`, `snapshot_carries_ticket_metadata`, `snapshot_is_read_only`, updates après cancel/resolve. 3. `cargo test -p application --test workstate` - Exit 0. - `running 12 tests` - `test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Couvre `workstate_lists_two_tickets_in_fifo_order`, `workstate_marks_busy_head_in_progress_and_rest_queued`, `workstate_marks_all_queued_when_agent_idle`, `workstate_maps_human_and_agent_ticket_sources`, queue absente/agent hors manifest. 4. `cargo test -p app-tauri --test dto_agents` - Exit 0. - `running 20 tests` - `test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Couvre `project_work_state_dto_serialises_tickets_camelcase`: `tickets`, `inProgress`, source `agent.agentId`, source `human`, absence de snake_case. 5. `cargo check -p app-tauri` - Exit 0. - `Finished dev profile [unoptimized + debuginfo] target(s) in 0.59s` 6. `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Exit 0. - Warnings Vite non bloquants: options `esbuild` dépréciées/ignorées au profit de `oxc`. - `Test Files 2 passed (2)` - `Tests 17 passed (17)` - `Duration 2.02s` 7. `cd frontend && npx tsc --noEmit` - Exit 0, aucune sortie. 8. Commande QA ajoutée, car `crates/domain/src/mailbox.rs` est touché par le lot: `cargo test -p domain mailbox --lib` - Exit 0. - `running 6 tests` - `test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 170 filtered out; finished in 0.00s` Vérifications de périmètre: - Backend/Tauri: - Port read-only présent: `domain::mailbox::AgentQueueSnapshot` dans `crates/domain/src/mailbox.rs:69`. - `InMemoryMailbox` implémente la snapshot read-only dans `crates/infrastructure/src/mailbox/mod.rs:166`; tests FIFO/métadonnées/read-only verts. - `GetProjectWorkState` consomme `Arc<dyn AgentQueueSnapshot>` dans `crates/application/src/workstate/mod.rs:130` et mappe `agents[].tickets`; tests application verts. - DTO `tickets` camelCase dans `crates/app-tauri/src/dto.rs:1602` et test camelCase dédié vert. - Wiring `AppState`: même `InMemoryMailbox` exposé comme `AgentMailbox` et `AgentQueueSnapshot` dans `crates/app-tauri/src/state.rs:994-999`, puis injecté dans `GetProjectWorkState::new` vers `state.rs:1034-1038`. - Frontend: - Types `tickets` présents dans `frontend/src/domain/index.ts:153-179`. - Mock normalisé avec fallback `tickets: []` dans `frontend/src/adapters/mock/index.ts:1670-1684`. - Panneau Work trie les tickets par `position` avant affichage dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx:65`. - UI distingue source humaine et agent: `Human` pour `source.kind === "human"`, sinon `label (agent-id-prefix)` dans `ProjectWorkStatePanel.tsx:21-27`; test `renders an in-progress human ticket` et test FIFO agent verts. - Refresh sur `delegationReady` couvert par `frontend/src/features/workstate/workstate.test.tsx:223` et vert. Anomalies: aucune anomalie bloquante détectée. Les seuls messages observés sont les warnings Vite sur la config `esbuild` dépréciée, sans échec de test ni impact Lot B constaté.

View File

@ -62,3 +62,5 @@
{"id":"aab45df1-f0ac-4eca-850b-c89b688c3211","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781945801522,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict QA final : vert accepté avec réserve environnementale.\n\nJe ne maintiens pas le KO global, car le rouge restant est maintenant expliqué et borné : dans cet environnement sandbox, un bind de socket Unix réel échoue `PermissionDenied` même pour un smoke minimal `UnixListener::bind` dans `/tmp` et `/run/user/1000`. Les tests qui exigent un vrai socket Unix loopback ne peuvent donc pas être probants ici. Le reste du périmètre applicatif, hotfix submit/logs, frontend, wiring hors vrai socket et app-tauri lib hors tests loopback réels est vert.\n\nCommandes vertes déjà validées QA sur ce périmètre :\n\n```text\ncargo test -p application compose_convention_file --no-fail-fast\n```\nRésultat : `15 passed; 0 failed; 28 filtered out`.\n\n```text\ncargo test -p application\n```\nRésultat : `43 passed; 0 failed` pour les unit tests application, tous les tests dintégration application et doctests passés. Warning existant : `method writes is never used` dans `crates/application/tests/orchestrator_service.rs:1642`.\n\n```text\ncargo test -p infrastructure input\n```\nRésultat : `35 passed; 0 failed; 187 filtered out`.\n\n```text\ncargo test -p application --test orchestrator_service\n```\nRésultat : `45 passed; 0 failed` avec le même warning existant `writes`.\n\n```text\ncd frontend && npx vitest run src/features/terminals/useWritePortal.test.tsx\n```\nRésultat : `1 passed`, `12 tests passed`. Warnings Vite existants sur options `esbuild` dépréciées / `oxc` prioritaire.\n\n```text\ncd frontend && npx vitest run src/features/terminals/TerminalView.portal.test.tsx src/features/terminals/TerminalView.test.tsx src/features/terminals/useWritePortal.test.tsx\n```\nRésultat : `3 passed`, `24 tests passed`. Même warnings Vite.\n\n```text\ncd frontend && npx tsc --noEmit\n```\nRésultat : succès, aucune sortie.\n\n```text\ncargo check -p app-tauri\n```\nRésultat : succès, `Finished dev profile`.\n\nCommandes de confirmation après diagnostic sandbox, relancées par QA :\n\n```text\ncargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together\n```\nRésultat réel :\n```text\nrunning 9 tests\n...\ntest result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s\n```\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat réel :\n```text\nrunning 39 tests\n...\ntest result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s\n```\n\nRéserve exacte : non-validé dans ce sandbox uniquement pour les tests nécessitant la création/binding dun vrai socket Unix loopback :\n- `crates/app-tauri/tests/orchestrator_wiring.rs` : `open_binds_the_project_loopback_endpoint`, `double_open_keeps_a_single_endpoint_no_address_in_use`, `close_cleans_up_the_endpoint_socket_file`, `file_watcher_and_loopback_endpoint_live_together`.\n- `app-tauri --lib` : `state::mcp_e2e_loopback_tests`, `state::bind_endpoint_d1_tests`, `mcp_bridge::tests::end_to_end_over_real_loopback`.\n\nCes tests restent à exécuter dans un environnement non sandboxé permettant `UnixListener::bind` sur socket filesystem. Je nai modifié aucun fichier."} {"id":"aab45df1-f0ac-4eca-850b-c89b688c3211","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781945801522,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict QA final : vert accepté avec réserve environnementale.\n\nJe ne maintiens pas le KO global, car le rouge restant est maintenant expliqué et borné : dans cet environnement sandbox, un bind de socket Unix réel échoue `PermissionDenied` même pour un smoke minimal `UnixListener::bind` dans `/tmp` et `/run/user/1000`. Les tests qui exigent un vrai socket Unix loopback ne peuvent donc pas être probants ici. Le reste du périmètre applicatif, hotfix submit/logs, frontend, wiring hors vrai socket et app-tauri lib hors tests loopback réels est vert.\n\nCommandes vertes déjà validées QA sur ce périmètre :\n\n```text\ncargo test -p application compose_convention_file --no-fail-fast\n```\nRésultat : `15 passed; 0 failed; 28 filtered out`.\n\n```text\ncargo test -p application\n```\nRésultat : `43 passed; 0 failed` pour les unit tests application, tous les tests dintégration application et doctests passés. Warning existant : `method writes is never used` dans `crates/application/tests/orchestrator_service.rs:1642`.\n\n```text\ncargo test -p infrastructure input\n```\nRésultat : `35 passed; 0 failed; 187 filtered out`.\n\n```text\ncargo test -p application --test orchestrator_service\n```\nRésultat : `45 passed; 0 failed` avec le même warning existant `writes`.\n\n```text\ncd frontend && npx vitest run src/features/terminals/useWritePortal.test.tsx\n```\nRésultat : `1 passed`, `12 tests passed`. Warnings Vite existants sur options `esbuild` dépréciées / `oxc` prioritaire.\n\n```text\ncd frontend && npx vitest run src/features/terminals/TerminalView.portal.test.tsx src/features/terminals/TerminalView.test.tsx src/features/terminals/useWritePortal.test.tsx\n```\nRésultat : `3 passed`, `24 tests passed`. Même warnings Vite.\n\n```text\ncd frontend && npx tsc --noEmit\n```\nRésultat : succès, aucune sortie.\n\n```text\ncargo check -p app-tauri\n```\nRésultat : succès, `Finished dev profile`.\n\nCommandes de confirmation après diagnostic sandbox, relancées par QA :\n\n```text\ncargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together\n```\nRésultat réel :\n```text\nrunning 9 tests\n...\ntest result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s\n```\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat réel :\n```text\nrunning 39 tests\n...\ntest result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s\n```\n\nRéserve exacte : non-validé dans ce sandbox uniquement pour les tests nécessitant la création/binding dun vrai socket Unix loopback :\n- `crates/app-tauri/tests/orchestrator_wiring.rs` : `open_binds_the_project_loopback_endpoint`, `double_open_keeps_a_single_endpoint_no_address_in_use`, `close_cleans_up_the_endpoint_socket_file`, `file_watcher_and_loopback_endpoint_live_together`.\n- `app-tauri --lib` : `state::mcp_e2e_loopback_tests`, `state::bind_endpoint_d1_tests`, `mcp_bridge::tests::end_to_end_over_real_loopback`.\n\nCes tests restent à exécuter dans un environnement non sandboxé permettant `UnixListener::bind` sur socket filesystem. Je nai modifié aucun fichier."}
{"id":"ea85a3be-0cf0-4ad7-b8a7-7344426844b7","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781971470719,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA globale pour le chantier `feature/conversation-ux-live-state` Lot A. Périmètre implémenté: backend/Tauri read-model `get_project_work_state(projectId)` + frontend onglet/panneau Work read-only. Backend a ajouté `crates/application/src/workstate/mod.rs`, snapshots live, commande/DTO Tauri; frontend a ajouté types/domain/port/adapter/mock/hook/panel et onglet Work.\n\nMerci de relire le périmètre: read-only, pas nouvelle persistance, pas lecture log/handoff, pas FIFO complète, ordre manifeste, live agents hors manifeste ignorés. Exécute les tests pertinents réels. Minimum:\n- `cargo test -p application workstate`\n- `cargo test -p application`\n- `cargo check -p app-tauri`\n- tests app-tauri DTO/live pertinents (`dto_agents`, `list_live_agents_r0b`) et si nécessaire app-tauri lib avec skips socket connus\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n- `cd frontend && npx tsc --noEmit`\nRetourne verdict, commandes exactes/résultats, risques, et si les tests socket Unix restent filtrés pour raison sandbox déjà diagnostiquée. Ne modifie pas le code sauf correction de test strictement nécessaire."} {"id":"ea85a3be-0cf0-4ad7-b8a7-7344426844b7","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781971470719,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA globale pour le chantier `feature/conversation-ux-live-state` Lot A. Périmètre implémenté: backend/Tauri read-model `get_project_work_state(projectId)` + frontend onglet/panneau Work read-only. Backend a ajouté `crates/application/src/workstate/mod.rs`, snapshots live, commande/DTO Tauri; frontend a ajouté types/domain/port/adapter/mock/hook/panel et onglet Work.\n\nMerci de relire le périmètre: read-only, pas nouvelle persistance, pas lecture log/handoff, pas FIFO complète, ordre manifeste, live agents hors manifeste ignorés. Exécute les tests pertinents réels. Minimum:\n- `cargo test -p application workstate`\n- `cargo test -p application`\n- `cargo check -p app-tauri`\n- tests app-tauri DTO/live pertinents (`dto_agents`, `list_live_agents_r0b`) et si nécessaire app-tauri lib avec skips socket connus\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n- `cd frontend && npx tsc --noEmit`\nRetourne verdict, commandes exactes/résultats, risques, et si les tests socket Unix restent filtrés pour raison sandbox déjà diagnostiquée. Ne modifie pas le code sauf correction de test strictement nécessaire."}
{"id":"39f05602-a660-4632-accc-42d4887ce1ed","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781971551021,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels.\n\nPérimètre relu :\n- Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`.\n- Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète.\n- Ordre manifeste conservé par itération sur `manifest.entries`.\n- Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis.\n- Snapshot live typé ajouté sans changer `live_agents()` existant.\n- Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`.\n- Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants.\n\nCommandes exécutées et résultats :\n\n```text\ncargo fmt -p application -p app-tauri -- --check\n```\nRésultat : OK, aucune sortie.\n\n```text\ncargo test -p application workstate\n```\nRésultat :\n```text\nrunning 5 tests\ntest workstate_attaches_live_pty_session_to_manifest_agent ... ok\ntest workstate_includes_busy_state_from_input_mediator ... ok\ntest workstate_attaches_live_structured_session_to_manifest_agent ... ok\ntest workstate_ignores_live_agents_absent_from_manifest ... ok\ntest workstate_lists_manifest_agents_idle_without_live_sessions ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\nWarning existant observé :\n```text\nwarning: method `writes` is never used\n --> crates/application/tests/orchestrator_service.rs:1642:8\n```\n\n```text\ncargo test -p application\n```\nRésultat :\n```text\nrunning 43 tests\n...\ntest result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n...\nRunning tests/workstate.rs\nrunning 5 tests\n...\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n\nDoc-tests application\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\nMême warning existant `CapturingFs::writes` unused.\n\n```text\ncargo check -p app-tauri\n```\nRésultat :\n```text\nFinished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s\n```\n\n```text\ncargo test -p app-tauri --test dto_agents\n```\nRésultat :\n```text\nrunning 19 tests\n...\ntest project_work_state_dto_serialises_live_and_busy_camelcase ... ok\n...\ntest result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\n\n```text\ncargo test -p app-tauri --test list_live_agents_r0b\n```\nRésultat :\n```text\nrunning 5 tests\ntest no_sessions_yields_empty_list ... ok\ntest both_kinds_live_listed_without_duplicates ... ok\ntest pty_live_agent_is_listed ... ok\ntest same_agent_in_both_registries_is_deduplicated ... ok\ntest structured_live_agent_is_listed ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat :\n```text\nrunning 39 tests\n...\ntest result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s\n```\nLes 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A.\n\n```text\ncd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx\n```\nRésultat :\n```text\nTest Files 2 passed (2)\nTests 13 passed (13)\n```\nWarnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire.\n\n```text\ncd frontend && npx tsc --noEmit\n```\nRésultat : succès, aucune sortie.\n\nRisques restants :\n- Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas dhistorique conversationnel, pas de lecture log/handoff.\n- Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem.\n- Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot.\n\nAucune correction requise. Je nai modifié aucun fichier."} {"id":"39f05602-a660-4632-accc-42d4887ce1ed","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781971551021,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels.\n\nPérimètre relu :\n- Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`.\n- Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète.\n- Ordre manifeste conservé par itération sur `manifest.entries`.\n- Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis.\n- Snapshot live typé ajouté sans changer `live_agents()` existant.\n- Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`.\n- Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants.\n\nCommandes exécutées et résultats :\n\n```text\ncargo fmt -p application -p app-tauri -- --check\n```\nRésultat : OK, aucune sortie.\n\n```text\ncargo test -p application workstate\n```\nRésultat :\n```text\nrunning 5 tests\ntest workstate_attaches_live_pty_session_to_manifest_agent ... ok\ntest workstate_includes_busy_state_from_input_mediator ... ok\ntest workstate_attaches_live_structured_session_to_manifest_agent ... ok\ntest workstate_ignores_live_agents_absent_from_manifest ... ok\ntest workstate_lists_manifest_agents_idle_without_live_sessions ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\nWarning existant observé :\n```text\nwarning: method `writes` is never used\n --> crates/application/tests/orchestrator_service.rs:1642:8\n```\n\n```text\ncargo test -p application\n```\nRésultat :\n```text\nrunning 43 tests\n...\ntest result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n...\nRunning tests/workstate.rs\nrunning 5 tests\n...\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n\nDoc-tests application\nrunning 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\nMême warning existant `CapturingFs::writes` unused.\n\n```text\ncargo check -p app-tauri\n```\nRésultat :\n```text\nFinished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s\n```\n\n```text\ncargo test -p app-tauri --test dto_agents\n```\nRésultat :\n```text\nrunning 19 tests\n...\ntest project_work_state_dto_serialises_live_and_busy_camelcase ... ok\n...\ntest result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\n\n```text\ncargo test -p app-tauri --test list_live_agents_r0b\n```\nRésultat :\n```text\nrunning 5 tests\ntest no_sessions_yields_empty_list ... ok\ntest both_kinds_live_listed_without_duplicates ... ok\ntest pty_live_agent_is_listed ... ok\ntest same_agent_in_both_registries_is_deduplicated ... ok\ntest structured_live_agent_is_listed ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n```\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat :\n```text\nrunning 39 tests\n...\ntest result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s\n```\nLes 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A.\n\n```text\ncd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx\n```\nRésultat :\n```text\nTest Files 2 passed (2)\nTests 13 passed (13)\n```\nWarnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire.\n\n```text\ncd frontend && npx tsc --noEmit\n```\nRésultat : succès, aucune sortie.\n\nRisques restants :\n- Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas dhistorique conversationnel, pas de lecture log/handoff.\n- Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem.\n- Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot.\n\nAucune correction requise. Je nai modifié aucun fichier."}
{"id":"81d3494b-a647-40ea-909d-7b4649094e81","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781974100258,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Valide le Lot B complet `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`.\n\nPérimètre à vérifier:\n- Backend/Tauri: port read-only `AgentQueueSnapshot`, snapshots FIFO d'`InMemoryMailbox`, `GetProjectWorkState.agents[].tickets`, DTO camelCase, wiring `AppState`.\n- Frontend: types `tickets`, mock normalisé, panneau Work affiche tickets FIFO inProgress/queued, refresh sur `delegationReady`.\n- Les tickets human et agent sont inclus; l'UI ne doit pas les présenter tous comme délégations agent.\n\nCommandes minimales demandées (adapte si le repo impose un cwd):\n- `cargo fmt --all -- --check`\n- `cargo test -p infrastructure mailbox --lib`\n- `cargo test -p application --test workstate`\n- `cargo test -p app-tauri --test dto_agents`\n- `cargo check -p app-tauri`\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n- `cd frontend && npx tsc --noEmit`\n\nTu peux ajouter des commandes pertinentes, mais ne committe pas et ne modifie pas les fichiers applicatifs sauf tests QA indispensables. Ignore le dirty runtime `.ideai/*` s'il est seulement live-session.\n\nRetour attendu: verdict clair, commandes exécutées avec résultats réels, anomalies éventuelles avec fichiers/lignes, et si rouge, donne la sortie d'erreur exploitable."}
{"id":"b916af4a-6f94-4777-92b1-c5f89f898cde","conversation":"08336578-5b47-09d2-2f41-5cc483f101f4","atMs":1781974271224,"source":{"kind":"agent","agentId":"aefdbd61-e3d4-4bc1-9f42-c259446a97b5"},"role":"response","text":"Verdict: VERT pour le Lot B `workstate delegation/queue snapshot` sur `feature/workstate-delegation-queue`.\n\nJe nai modifié aucun fichier. Dirty ignoré côté runtime `.ideai/*`; les fichiers applicatifs/test du lot étaient déjà modifiés dans le worktree.\n\nCommandes exécutées et résultats réels:\n\n1. `cargo fmt --all -- --check`\n - Exit 0, aucune sortie.\n\n2. `cargo test -p infrastructure mailbox --lib`\n - Exit 0.\n - `running 13 tests`\n - `test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 215 filtered out; finished in 0.00s`\n - Couvre notamment `snapshot_preserves_fifo_order_and_positions`, `snapshot_carries_ticket_metadata`, `snapshot_is_read_only`, updates après cancel/resolve.\n\n3. `cargo test -p application --test workstate`\n - Exit 0.\n - `running 12 tests`\n - `test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s`\n - Couvre `workstate_lists_two_tickets_in_fifo_order`, `workstate_marks_busy_head_in_progress_and_rest_queued`, `workstate_marks_all_queued_when_agent_idle`, `workstate_maps_human_and_agent_ticket_sources`, queue absente/agent hors manifest.\n\n4. `cargo test -p app-tauri --test dto_agents`\n - Exit 0.\n - `running 20 tests`\n - `test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s`\n - Couvre `project_work_state_dto_serialises_tickets_camelcase`: `tickets`, `inProgress`, source `agent.agentId`, source `human`, absence de snake_case.\n\n5. `cargo check -p app-tauri`\n - Exit 0.\n - `Finished dev profile [unoptimized + debuginfo] target(s) in 0.59s`\n\n6. `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n - Exit 0.\n - Warnings Vite non bloquants: options `esbuild` dépréciées/ignorées au profit de `oxc`.\n - `Test Files 2 passed (2)`\n - `Tests 17 passed (17)`\n - `Duration 2.02s`\n\n7. `cd frontend && npx tsc --noEmit`\n - Exit 0, aucune sortie.\n\n8. Commande QA ajoutée, car `crates/domain/src/mailbox.rs` est touché par le lot: `cargo test -p domain mailbox --lib`\n - Exit 0.\n - `running 6 tests`\n - `test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 170 filtered out; finished in 0.00s`\n\nVérifications de périmètre:\n\n- Backend/Tauri:\n - Port read-only présent: `domain::mailbox::AgentQueueSnapshot` dans `crates/domain/src/mailbox.rs:69`.\n - `InMemoryMailbox` implémente la snapshot read-only dans `crates/infrastructure/src/mailbox/mod.rs:166`; tests FIFO/métadonnées/read-only verts.\n - `GetProjectWorkState` consomme `Arc<dyn AgentQueueSnapshot>` dans `crates/application/src/workstate/mod.rs:130` et mappe `agents[].tickets`; tests application verts.\n - DTO `tickets` camelCase dans `crates/app-tauri/src/dto.rs:1602` et test camelCase dédié vert.\n - Wiring `AppState`: même `InMemoryMailbox` exposé comme `AgentMailbox` et `AgentQueueSnapshot` dans `crates/app-tauri/src/state.rs:994-999`, puis injecté dans `GetProjectWorkState::new` vers `state.rs:1034-1038`.\n\n- Frontend:\n - Types `tickets` présents dans `frontend/src/domain/index.ts:153-179`.\n - Mock normalisé avec fallback `tickets: []` dans `frontend/src/adapters/mock/index.ts:1670-1684`.\n - Panneau Work trie les tickets par `position` avant affichage dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx:65`.\n - UI distingue source humaine et agent: `Human` pour `source.kind === \"human\"`, sinon `label (agent-id-prefix)` dans `ProjectWorkStatePanel.tsx:21-27`; test `renders an in-progress human ticket` et test FIFO agent verts.\n - Refresh sur `delegationReady` couvert par `frontend/src/features/workstate/workstate.test.tsx:223` et vert.\n\nAnomalies: aucune anomalie bloquante détectée. Les seuls messages observés sont les warnings Vite sur la config `esbuild` dépréciée, sans échec de test ni impact Lot B constaté."}

View File

@ -1,5 +1,5 @@
--- ---
upTo: a0b9d44f-2761-4652-b2f3-28222dbc4c62 upTo: 7d70c87a-e7b5-468a-90c7-41decf1185bc
objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e
--- ---
**Objectif :** [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e **Objectif :** [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e
@ -18,3 +18,5 @@ objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu r
- **Response:** Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés). ## Résultats des commandes - `npx tsc --noEmit` : **propre** (0 erreur). - `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**. ## Nombre réel de gateways constaté : **14** Le `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés. ## Helper de conversion exact (pur, exporté, testé) ```ts // AgentLimitBadge.tsx export function timeInputToEpochMs(value: string, now: number): number | null { const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim()); if (!match) return null; const hours = Number(match[1]); const minutes = Number(match[2]); if (hours > 23 || minutes > 59) return null; const d = new Date(now); d.setHours(hours, minutes, 0, 0); return d.getTime(); } ``` Comportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée). ═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══ Fichiers modifiés : 1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise<void>` ajouté à `InputGateway`, à côté de `cancelResume`. 2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke("set_resume_at", { agentId, resetsAtMs })` (args nus, comme cancel_resume). 3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`). 4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant). 5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type="time"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**. 6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge. 7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)* ═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══ Fichier modifié : - `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `"permission"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier. ## Point de friction / note Petit ajustement non listé mais nécessaire : le `<form>` portait initialement le même `aria-label="schedule resume"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `"resume time form"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle. Le filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`). - **Response:** Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés). ## Résultats des commandes - `npx tsc --noEmit` : **propre** (0 erreur). - `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**. ## Nombre réel de gateways constaté : **14** Le `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés. ## Helper de conversion exact (pur, exporté, testé) ```ts // AgentLimitBadge.tsx export function timeInputToEpochMs(value: string, now: number): number | null { const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim()); if (!match) return null; const hours = Number(match[1]); const minutes = Number(match[2]); if (hours > 23 || minutes > 59) return null; const d = new Date(now); d.setHours(hours, minutes, 0, 0); return d.getTime(); } ``` Comportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée). ═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══ Fichiers modifiés : 1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise<void>` ajouté à `InputGateway`, à côté de `cancelResume`. 2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke("set_resume_at", { agentId, resetsAtMs })` (args nus, comme cancel_resume). 3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`). 4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant). 5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type="time"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**. 6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge. 7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)* ═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══ Fichier modifié : - `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `"permission"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier. ## Point de friction / note Petit ajustement non listé mais nécessaire : le `<form>` portait initialement le même `aria-label="schedule resume"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `"resume time form"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle. Le filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`).
- **Prompt:** Implémente le Lot A frontend du chantier `UX conversations/délégations + read-model live-state minimal`, en consommant le backend/Tauri que DevBackend vient d'ajouter. Contrat backend disponible: - commande Tauri `get_project_work_state(projectId)`. - DTO `ProjectWorkStateDto { agents }` camelCase. - agent: `agentId`, `name`, `profileId`, `live?: { nodeId, sessionId, kind: "pty"|"structured" }`, `busy` taggé `{ state: "idle" }` ou `{ state: "busy", ticket, sinceMs }`. Périmètre frontend strict: - Ajouter types domaine `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - Ajouter port `WorkStateGateway` et adapter Tauri `workState`. - Ajouter mock gateway. - Ajouter feature `features/workstate`: hook `useProjectWorkState(projectId)` + `ProjectWorkStatePanel`. - Ajouter une surface UI minimale, probablement un onglet/sidebar `Work` dans `ProjectsView`, read-only: ligne par agent, live/offline, idle/busy, ticket court si busy, empty/error/refresh. - Refresh initial + refresh sur events existants pertinents si le système d'events est déjà accessible (`agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed` si disponible). Ne crée pas une grosse architecture event si ce n'est pas déjà le pattern. - Pas d'actions attach/stop/voir conversation dans ce lot. Tests attendus: - panel empty state. - agent idle/offline. - agent live + busy avec ticket court. - refresh déclenché par event si implémenté. - onglet Work visible/rendu dans ProjectsView si raisonnable. Exécute `npx vitest` ciblé et `npx tsc --noEmit`. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques. - **Prompt:** Implémente le Lot A frontend du chantier `UX conversations/délégations + read-model live-state minimal`, en consommant le backend/Tauri que DevBackend vient d'ajouter. Contrat backend disponible: - commande Tauri `get_project_work_state(projectId)`. - DTO `ProjectWorkStateDto { agents }` camelCase. - agent: `agentId`, `name`, `profileId`, `live?: { nodeId, sessionId, kind: "pty"|"structured" }`, `busy` taggé `{ state: "idle" }` ou `{ state: "busy", ticket, sinceMs }`. Périmètre frontend strict: - Ajouter types domaine `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - Ajouter port `WorkStateGateway` et adapter Tauri `workState`. - Ajouter mock gateway. - Ajouter feature `features/workstate`: hook `useProjectWorkState(projectId)` + `ProjectWorkStatePanel`. - Ajouter une surface UI minimale, probablement un onglet/sidebar `Work` dans `ProjectsView`, read-only: ligne par agent, live/offline, idle/busy, ticket court si busy, empty/error/refresh. - Refresh initial + refresh sur events existants pertinents si le système d'events est déjà accessible (`agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed` si disponible). Ne crée pas une grosse architecture event si ce n'est pas déjà le pattern. - Pas d'actions attach/stop/voir conversation dans ce lot. Tests attendus: - panel empty state. - agent idle/offline. - agent live + busy avec ticket court. - refresh déclenché par event si implémenté. - onglet Work visible/rendu dans ProjectsView si raisonnable. Exécute `npx vitest` ciblé et `npx tsc --noEmit`. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques.
- **Response:** Lot A frontend implémenté, sans commit. Fichiers modifiés/ajoutés frontend: - `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`. - `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`. - `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`. - `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock. - `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`. - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court. - `frontend/src/features/workstate/index.ts`: export feature. - `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event. - `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`. - `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu. Commandes exécutées: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Résultat: OK, 2 fichiers passés, 13 tests passés. - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`. - `cd frontend && npx tsc --noEmit` - Résultat: OK, aucune erreur. Risques / limites: - Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent. - UI strictement read-only comme demandé: pas dattach/stop/voir conversation. - Je nai pas lancé tout le `vitest` global, seulement le ciblé demandé. - Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés. - **Response:** Lot A frontend implémenté, sans commit. Fichiers modifiés/ajoutés frontend: - `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`. - `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`. - `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`. - `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock. - `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`. - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court. - `frontend/src/features/workstate/index.ts`: export feature. - `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event. - `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`. - `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu. Commandes exécutées: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Résultat: OK, 2 fichiers passés, 13 tests passés. - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`. - `cd frontend && npx tsc --noEmit` - Résultat: OK, aucune erreur. Risques / limites: - Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent. - UI strictement read-only comme demandé: pas dattach/stop/voir conversation. - Je nai pas lancé tout le `vitest` global, seulement le ciblé demandé. - Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés.
- **Prompt:** Implémente le Lot B frontend `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend. Contexte backend disponible: - `ProjectWorkState.agents[].tickets` est maintenant exposé côté DTO camelCase. - Ticket fields TS attendus: `ticketId`, `conversationId`, `position`, `status` (`"inProgress" | "queued"`), `source` (`{ kind: "human" } | { kind: "agent"; agentId: string }`), `requesterLabel`, `taskPreview`, `taskLen`. - Backend inclut les tickets `human` et `agent`; l'UI doit donc libeller clairement "tickets"/"inputs" ou gérer Human/Agent sans prétendre que tout est délégation agent. À faire: 1. Étendre les types frontend dans `frontend/src/domain/index.ts` (`AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`, `AgentWorkState.tickets`). 2. Adapter le mock gateway pour accepter/normaliser `tickets` si nécessaire. 3. Dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, afficher sous chaque agent une mini-liste compacte des tickets FIFO: - `#1 In progress` ou `#2 Queued` (position + 1), badge visuel cohérent avec l'existant, - requester/source (`Human` pour source human, label requester et/ou agent id court pour agent), - `taskPreview`, indication légère si `taskLen > taskPreview.length`, ticket id court monospace, - agent sans ticket: ne pas ajouter de bruit visuel excessif. 4. Dans le hook `useProjectWorkState`, ajouter un refresh sur l'event `delegationReady` si l'infrastructure d'events frontend existe déjà pour les autres events; sinon explique précisément pourquoi. 5. Mettre à jour/ajouter les tests Vitest pertinents: agent avec ticket inProgress, deux tickets en ordre, agent sans ticket, event `delegationReady` déclenche refresh, mock renvoie tickets. Contraintes: - Respecte le design existant, pas d'actions d'annulation/résolution, pas de nouvelle commande Tauri, pas de lecture logs/handoff. - Ne committe pas. - Ignore les fichiers runtime `.ideai/*` dirty. Vérifications demandées: `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`, puis `cd frontend && npx tsc --noEmit`. Retourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage.
- **Response:** Lot B frontend `workstate delegation/queue snapshot` implémenté sur la branche active `feature/workstate-delegation-queue`. Aucun commit effectué. Fichiers modifiés: - `frontend/src/domain/index.ts` - `frontend/src/adapters/mock/index.ts` - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` - `frontend/src/features/workstate/useProjectWorkState.ts` - `frontend/src/features/workstate/workstate.test.tsx` Choix UI: - Sous-liste compacte uniquement pour les agents qui ont des tickets, sans bruit visuel pour les agents sans ticket. - Tickets triés par `position`, affichés en `#N In progress` / `#N Queued`. - Source humaine libellée `Human`; source agent affichée avec `requesterLabel` + id agent court quand disponible. - `taskPreview` affiché inline, avec suffixe léger `+N` si `taskLen > taskPreview.length`. - `ticketId` court en monospace avec `title` complet. Détails techniques: - Ajout des types `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource` et `AgentWorkState.tickets`. - `MockWorkStateGateway` accepte et normalise les états legacy sans `tickets` en `tickets: []`. - `useProjectWorkState` rafraîchit maintenant aussi sur lévénement domaine `delegationReady`, via linfrastructure `SystemGateway.onDomainEvent` déjà existante. Vérifications réelles: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `17 passed`. - `cd frontend && npx tsc --noEmit` : OK, aucune sortie derreur. Blocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé.

View File

@ -12,3 +12,5 @@
{"id":"50c3e999-c6a3-4d8b-aef3-b6c273ed9afc","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781677152871,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés).\n\n## Résultats des commandes\n- `npx tsc --noEmit` : **propre** (0 erreur).\n- `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**.\n\n## Nombre réel de gateways constaté : **14**\nLe `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés.\n\n## Helper de conversion exact (pur, exporté, testé)\n```ts\n// AgentLimitBadge.tsx\nexport function timeInputToEpochMs(value: string, now: number): number | null {\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(value.trim());\n if (!match) return null;\n const hours = Number(match[1]);\n const minutes = Number(match[2]);\n if (hours > 23 || minutes > 59) return null;\n const d = new Date(now);\n d.setHours(hours, minutes, 0, 0);\n return d.getTime();\n}\n```\nComportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée).\n\n═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══\nFichiers modifiés :\n1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise<void>` ajouté à `InputGateway`, à côté de `cancelResume`.\n2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke(\"set_resume_at\", { agentId, resetsAtMs })` (args nus, comme cancel_resume).\n3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`).\n4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant).\n5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type=\"time\"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**.\n6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge.\n7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)*\n\n═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══\nFichier modifié :\n- `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `\"permission\"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier.\n\n## Point de friction / note\nPetit ajustement non listé mais nécessaire : le `<form>` portait initialement le même `aria-label=\"schedule resume\"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `\"resume time form\"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle.\n\nLe filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`)."} {"id":"50c3e999-c6a3-4d8b-aef3-b6c273ed9afc","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781677152871,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Les deux livrables sont prêts. NON commité (Git tranche, 2 commits séparés).\n\n## Résultats des commandes\n- `npx tsc --noEmit` : **propre** (0 erreur).\n- `npx vitest run src/features/agents/ src/adapters/mock/` : **109 tests / 8 fichiers, tous verts**.\n\n## Nombre réel de gateways constaté : **14**\nLe `Gateways` (ports/index.ts) en a 14 ; l'array du test en listait 13 — il manquait `permission` (présente dans HEAD). Compteur + libellé + array corrigés.\n\n## Helper de conversion exact (pur, exporté, testé)\n```ts\n// AgentLimitBadge.tsx\nexport function timeInputToEpochMs(value: string, now: number): number | null {\n const match = /^(\\d{1,2}):(\\d{2})$/.exec(value.trim());\n if (!match) return null;\n const hours = Number(match[1]);\n const minutes = Number(match[2]);\n if (hours > 23 || minutes > 59) return null;\n const d = new Date(now);\n d.setHours(hours, minutes, 0, 0);\n return d.getTime();\n}\n```\nComportement documenté : input `time` seul ⇒ aujourd'hui à HH:MM (même jour calendaire que `now`). Une heure déjà passée renvoie un instant < now **sans validation stricte** — le backend clampe à maintenant ⇒ reprise immédiate (commenté dans le code et couvert par un test). Empty/malformé/`25:00`/`12:60` ⇒ `null` (saisie incomplète ignorée).\n\n═══ LIVRABLE 1 — LS8-front (commit « LS8-front ») ═══\nFichiers modifiés :\n1. `frontend/src/ports/index.ts` — `setResumeAt(agentId, resetsAtMs): Promise<void>` ajouté à `InputGateway`, à côté de `cancelResume`.\n2. `frontend/src/adapters/input.ts` — `setResumeAt` → `invoke(\"set_resume_at\", { agentId, resetsAtMs })` (args nus, comme cancel_resume).\n3. `frontend/src/adapters/mock/index.ts` — `MockInputGateway.setResumeAt` enregistre dans `resumeArmings: { agentId, resetsAtMs }[]` (patron `cancelledResumes`).\n4. `frontend/src/features/agents/useAgents.ts` — action `setResumeAt(agentId, resetsAtMs)` exposée, délègue au port, **aucune mutation optimiste** (l'event `agentResumeScheduled` rebascule l'état via l'abonnement existant).\n5. `frontend/src/features/agents/AgentLimitBadge.tsx` — état SUSPECTED sans heure (`suspected===true && !limitedUntil && !resumeFireAt`) : le placeholder/TODO LS7 est remplacé par un **formulaire** (input `type=\"time\"` aria-label `resume time` + bouton `Programmer la reprise` aria-label `schedule resume`, désactivés pendant `busy`/saisie vide). Soumission → `timeInputToEpochMs` → `onSetResumeAt(epochMs)`. Une fois armé, le badge bascule **tout seul** vers « limité jusqu'à HH:MM » + compte à rebours + Annuler (chaîne LS7 inchangée). TODO(LS-front) **retiré**.\n6. `frontend/src/features/agents/AgentsPanel.tsx` — passe `onSetResumeAt={(ms) => void vm.setResumeAt(a.id, ms)}` au badge.\n7. `frontend/src/features/agents/AgentLimitBadge.test.tsx` — **test QA préexistant (LS7) mis à jour au nouveau contrat** (prop requise `onSetResumeAt`, le cas « suspected sans heure » teste maintenant le formulaire au lieu de l'ancien texte placeholder) + ajout couverture LS8 : `timeInputToEpochMs` (jour calendaire, instant passé, null malformé), soumission du formulaire ⇒ `onSetResumeAt` appelé avec l'epoch-ms attendu, bouton désactivé si vide. *(Sans cette MAJ le typecheck cassait — la prop est devenue obligatoire et le rendu du cas suspected a changé. Aucune logique de prod altérée, seulement l'alignement du test.)*\n\n═══ LIVRABLE 2 — fix test (commit « fix-test » séparé) ═══\nFichier modifié :\n- `frontend/src/adapters/mock/mock.test.ts` — « thirteen » → « fourteen », `\"permission\"` inséré dans l'array attendu (ordre alphabétique). **Rien d'autre touché** dans ce fichier.\n\n## Point de friction / note\nPetit ajustement non listé mais nécessaire : le `<form>` portait initialement le même `aria-label=\"schedule resume\"` que son bouton submit, ce qui faisait matcher 2 éléments avec `getByLabelText`. J'ai renommé le label du form en `\"resume time form\"` (le bouton garde `schedule resume`). Aucune incidence fonctionnelle.\n\nLe filet humain niveau 3 est désormais **complet** : détection → formulaire de saisie → arme la reprise annulable via `set_resume_at` → bascule auto vers l'état nominal. Prêt pour QA (logique d'état dans le hook, helpers purs isolés, mocks instrumentés `resumeArmings`/`cancelledResumes`)."}
{"id":"b84fc73b-74fc-465f-a9b2-e4b79651e0f0","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781971246800,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot A frontend du chantier `UX conversations/délégations + read-model live-state minimal`, en consommant le backend/Tauri que DevBackend vient d'ajouter.\n\nContrat backend disponible:\n- commande Tauri `get_project_work_state(projectId)`.\n- DTO `ProjectWorkStateDto { agents }` camelCase.\n- agent: `agentId`, `name`, `profileId`, `live?: { nodeId, sessionId, kind: \"pty\"|\"structured\" }`, `busy` taggé `{ state: \"idle\" }` ou `{ state: \"busy\", ticket, sinceMs }`.\n\nPérimètre frontend strict:\n- Ajouter types domaine `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`.\n- Ajouter port `WorkStateGateway` et adapter Tauri `workState`.\n- Ajouter mock gateway.\n- Ajouter feature `features/workstate`: hook `useProjectWorkState(projectId)` + `ProjectWorkStatePanel`.\n- Ajouter une surface UI minimale, probablement un onglet/sidebar `Work` dans `ProjectsView`, read-only: ligne par agent, live/offline, idle/busy, ticket court si busy, empty/error/refresh.\n- Refresh initial + refresh sur events existants pertinents si le système d'events est déjà accessible (`agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed` si disponible). Ne crée pas une grosse architecture event si ce n'est pas déjà le pattern.\n- Pas d'actions attach/stop/voir conversation dans ce lot.\n\nTests attendus:\n- panel empty state.\n- agent idle/offline.\n- agent live + busy avec ticket court.\n- refresh déclenché par event si implémenté.\n- onglet Work visible/rendu dans ProjectsView si raisonnable.\n\nExécute `npx vitest` ciblé et `npx tsc --noEmit`. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques."} {"id":"b84fc73b-74fc-465f-a9b2-e4b79651e0f0","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781971246800,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot A frontend du chantier `UX conversations/délégations + read-model live-state minimal`, en consommant le backend/Tauri que DevBackend vient d'ajouter.\n\nContrat backend disponible:\n- commande Tauri `get_project_work_state(projectId)`.\n- DTO `ProjectWorkStateDto { agents }` camelCase.\n- agent: `agentId`, `name`, `profileId`, `live?: { nodeId, sessionId, kind: \"pty\"|\"structured\" }`, `busy` taggé `{ state: \"idle\" }` ou `{ state: \"busy\", ticket, sinceMs }`.\n\nPérimètre frontend strict:\n- Ajouter types domaine `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`.\n- Ajouter port `WorkStateGateway` et adapter Tauri `workState`.\n- Ajouter mock gateway.\n- Ajouter feature `features/workstate`: hook `useProjectWorkState(projectId)` + `ProjectWorkStatePanel`.\n- Ajouter une surface UI minimale, probablement un onglet/sidebar `Work` dans `ProjectsView`, read-only: ligne par agent, live/offline, idle/busy, ticket court si busy, empty/error/refresh.\n- Refresh initial + refresh sur events existants pertinents si le système d'events est déjà accessible (`agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed` si disponible). Ne crée pas une grosse architecture event si ce n'est pas déjà le pattern.\n- Pas d'actions attach/stop/voir conversation dans ce lot.\n\nTests attendus:\n- panel empty state.\n- agent idle/offline.\n- agent live + busy avec ticket court.\n- refresh déclenché par event si implémenté.\n- onglet Work visible/rendu dans ProjectsView si raisonnable.\n\nExécute `npx vitest` ciblé et `npx tsc --noEmit`. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques."}
{"id":"a0b9d44f-2761-4652-b2f3-28222dbc4c62","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781971459114,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot A frontend implémenté, sans commit.\n\nFichiers modifiés/ajoutés frontend:\n- `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`.\n- `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`.\n- `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`.\n- `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`.\n- `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock.\n- `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`.\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court.\n- `frontend/src/features/workstate/index.ts`: export feature.\n- `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event.\n- `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`.\n- `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu.\n\nCommandes exécutées:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n - Résultat: OK, 2 fichiers passés, 13 tests passés.\n - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`.\n- `cd frontend && npx tsc --noEmit`\n - Résultat: OK, aucune erreur.\n\nRisques / limites:\n- Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent.\n- UI strictement read-only comme demandé: pas dattach/stop/voir conversation.\n- Je nai pas lancé tout le `vitest` global, seulement le ciblé demandé.\n- Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés."} {"id":"a0b9d44f-2761-4652-b2f3-28222dbc4c62","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781971459114,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot A frontend implémenté, sans commit.\n\nFichiers modifiés/ajoutés frontend:\n- `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`.\n- `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`.\n- `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`.\n- `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`.\n- `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock.\n- `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`.\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court.\n- `frontend/src/features/workstate/index.ts`: export feature.\n- `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event.\n- `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`.\n- `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu.\n\nCommandes exécutées:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n - Résultat: OK, 2 fichiers passés, 13 tests passés.\n - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`.\n- `cd frontend && npx tsc --noEmit`\n - Résultat: OK, aucune erreur.\n\nRisques / limites:\n- Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent.\n- UI strictement read-only comme demandé: pas dattach/stop/voir conversation.\n- Je nai pas lancé tout le `vitest` global, seulement le ciblé demandé.\n- Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés."}
{"id":"01871912-13d6-42dd-b4df-497f0a1a9703","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781973915788,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot B frontend `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend.\n\nContexte backend disponible:\n- `ProjectWorkState.agents[].tickets` est maintenant exposé côté DTO camelCase.\n- Ticket fields TS attendus: `ticketId`, `conversationId`, `position`, `status` (`\"inProgress\" | \"queued\"`), `source` (`{ kind: \"human\" } | { kind: \"agent\"; agentId: string }`), `requesterLabel`, `taskPreview`, `taskLen`.\n- Backend inclut les tickets `human` et `agent`; l'UI doit donc libeller clairement \"tickets\"/\"inputs\" ou gérer Human/Agent sans prétendre que tout est délégation agent.\n\nÀ faire:\n1. Étendre les types frontend dans `frontend/src/domain/index.ts` (`AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`, `AgentWorkState.tickets`).\n2. Adapter le mock gateway pour accepter/normaliser `tickets` si nécessaire.\n3. Dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, afficher sous chaque agent une mini-liste compacte des tickets FIFO:\n - `#1 In progress` ou `#2 Queued` (position + 1), badge visuel cohérent avec l'existant,\n - requester/source (`Human` pour source human, label requester et/ou agent id court pour agent),\n - `taskPreview`, indication légère si `taskLen > taskPreview.length`, ticket id court monospace,\n - agent sans ticket: ne pas ajouter de bruit visuel excessif.\n4. Dans le hook `useProjectWorkState`, ajouter un refresh sur l'event `delegationReady` si l'infrastructure d'events frontend existe déjà pour les autres events; sinon explique précisément pourquoi.\n5. Mettre à jour/ajouter les tests Vitest pertinents: agent avec ticket inProgress, deux tickets en ordre, agent sans ticket, event `delegationReady` déclenche refresh, mock renvoie tickets.\n\nContraintes:\n- Respecte le design existant, pas d'actions d'annulation/résolution, pas de nouvelle commande Tauri, pas de lecture logs/handoff.\n- Ne committe pas.\n- Ignore les fichiers runtime `.ideai/*` dirty.\n\nVérifications demandées: `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`, puis `cd frontend && npx tsc --noEmit`. Retourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage."}
{"id":"7d70c87a-e7b5-468a-90c7-41decf1185bc","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781974090422,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot B frontend `workstate delegation/queue snapshot` implémenté sur la branche active `feature/workstate-delegation-queue`. Aucun commit effectué.\n\nFichiers modifiés:\n- `frontend/src/domain/index.ts`\n- `frontend/src/adapters/mock/index.ts`\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`\n- `frontend/src/features/workstate/useProjectWorkState.ts`\n- `frontend/src/features/workstate/workstate.test.tsx`\n\nChoix UI:\n- Sous-liste compacte uniquement pour les agents qui ont des tickets, sans bruit visuel pour les agents sans ticket.\n- Tickets triés par `position`, affichés en `#N In progress` / `#N Queued`.\n- Source humaine libellée `Human`; source agent affichée avec `requesterLabel` + id agent court quand disponible.\n- `taskPreview` affiché inline, avec suffixe léger `+N` si `taskLen > taskPreview.length`.\n- `ticketId` court en monospace avec `title` complet.\n\nDétails techniques:\n- Ajout des types `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource` et `AgentWorkState.tickets`.\n- `MockWorkStateGateway` accepte et normalise les états legacy sans `tickets` en `tickets: []`.\n- `useProjectWorkState` rafraîchit maintenant aussi sur lévénement domaine `delegationReady`, via linfrastructure `SystemGateway.onDomainEvent` déjà existante.\n\nVérifications réelles:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `17 passed`.\n- `cd frontend && npx tsc --noEmit` : OK, aucune sortie derreur.\n\nBlocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé."}

View File

@ -1,11 +1,9 @@
--- ---
upTo: 95e2a7a2-bb92-474f-83ec-0a2a7bab2c3e upTo: 53773ad0-5bb6-4e24-8456-d7ab17be3db0
objective: Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est objective: Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est
--- ---
**Objectif :** Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est **Objectif :** Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est
- **Response:** Base remise au propre après le rebuild AppImage. Aucune action sortante, aucune suppression. ## Inspection Les modifications observées (3 affichées + 2 autres détectées) sont **100 % de l'état runtime/mémoire `.ideai/`**, zéro ligne de code source : - `.ideai/conversations/6bc594e8…/handoff.md` + `log.jsonl` : dérive de conversation. - `.ideai/layouts.json` : état de layout UI. - `.ideai/memory/MEMORY.md` : +1 ligne d'index. - `.ideai/memory/checkpoint-orchestrator-designation-appimage-build.md` (non suivi) : ton checkpoint de build. Vérifié : l'artefact `target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage` est **git-ignored (hors git)** → je n'y touche pas, comme demandé. ## Décision & action Pur runtime → **un seul commit `chore(wip)` sur `develop`**, directement (ce n'est pas une feature, pas de cycle Architect→Dev→QA requis), pour repartir d'une base propre sans rien laisser dirty ni perdre ton checkpoint : - `9c71a5b` **chore(wip): état runtime .ideai post-build AppImage 0.3.0** (conversation 6bc594e8 handoff+log, layouts, checkpoint de build + son index MEMORY.md). ## État final - Branche courante : **`develop`** @ `9c71a5b` (en avance de 9 sur `origin/develop`, non poussé). - `main` inchangé @ `29232dd`. - Worktree : **propre (0 dirty)**. `develop` est de nouveau une base saine pour le prochain chantier. Quand tu me l'annonces, je crée une `feature/* dédiée depuis develop`. Rappel : décision toujours en attente sur le ménage des branches `feature/agent-session-limits` (intégrée, supprimable), `feature/agent-skills`, `feature/agent-skill-awareness`, `fix/cold-start-delivery-race`.
- **Prompt:** Préparation du prochain chantier après fermeture de `orchestrator-designation`. Contexte: - Branche courante propre: `develop` @ `9c71a5b`, en avance de 9 sur `origin/develop`, aucun push. - `orchestrator-designation` est intégré et sa branche supprimée. - L'utilisateur a donné autonomie pour terminer les chantiers restants. Branches anciennes à statuer selon tes rappels: - `feature/agent-session-limits`: tu l'as qualifiée comme intégrée/supprimable. - `feature/agent-skills`: 1 commit non intégré. - `feature/agent-skill-awareness`: 5 commits non intégrés. - `fix/cold-start-delivery-race`: 3 commits non intégrés, probable sous-ensemble de skill-awareness. Ta mission Git: 1. Inspecte ces branches et leur relation à `develop`. 2. Prends la décision git la plus saine en autonomie: supprimer les branches déjà intégrées, conserver celles qui représentent un chantier réel, identifier les doublons/sous-ensembles. 3. Ne push pas. Pas d'action sortante. 4. Si une branche non intégrée doit être reprise comme prochain chantier, dis à Main laquelle et pourquoi. Si elle doit passer par Architect/Dev/QA avant merge, indique-le. 5. Rends compte des actions effectuées et de l'état final. Réponds via `idea_reply`.
- **Response:** Ménage des branches fait + cadrage du prochain chantier. Aucune action sortante, aucune suppression risquée. ## Action exécutée - **Supprimé `feature/agent-session-limits`** (`git branch -d`, refus auto si non mergée) : 0 commit propre, entièrement intégrée dans develop. Suppression sûre. ## Analyse des 3 branches restantes (relations git vérifiées) - **`feature/agent-skill-awareness`** — base `8452333`, 5 commits propres, NON intégrée. Le chantier le plus riche et récent : `ab34363` (manifeste de skills + outil MCP `idea_skill_read`), `e93a2c1` (fix cold-start delivery), `1a10d67` (test compteur outils MCP 11→12), `566bff4` (brief « capacités IdeA »), `5be8987` (runtime). **À conserver.** - **`feature/agent-skills`** — base `9736c42`, 1 commit `ef101db` (skills domain/usecases/FS store, L12), NON intégrée, **divergente** de skill-awareness (pas ancêtre). ⚠️ Elle touche **les mêmes fichiers** que skill-awareness (`domain/src/skill.rs`, `application/src/skill/*`, `infrastructure/src/store/skill.rs`) → ce sont très probablement **deux itérations concurrentes de la même couche skill**, pas des compléments. **Conservée**, mais l'arbitrage « laquelle fait foi » est une décision d'**architecture, pas git**. - **`fix/cold-start-delivery-race`** — base `6969dc7` (ligne main : elle traîne les commits de release 0.1.0/0.2.0). Son **seul contenu réel** est `9590eac` (fix cold-start), dont le **jumeau logique `e93a2c1` est déjà dans skill-awareness** (message identique). → **doublon superseded**. `git cherry` confirme qu'aucun de ces patchs n'est encore dans develop. ## Décisions / recommandations 1. **Prochain chantier = `feature/agent-skill-awareness`.** Pourquoi : c'est le travail le plus avancé et non intégré, et il englobe le fix cold-start de la branche `fix/cold-start-delivery-race`. 2. **Il DOIT passer par le cycle avant tout merge**, dans cet ordre : - **Architect d'abord** : trancher le chevauchement skill-awareness ⟷ `feature/agent-skills` (couche skill implémentée sur les mêmes fichiers). Décider qui est la source de vérité (probablement skill-awareness subsume agent-skills) avant de figer quoi que ce soit. - **Dev/QA** : revalider vert, en particulier `1a10d67` (compteur d'outils MCP, qui a déjà bougé 11→12 puis 12→13 dans state.rs côté develop) et le fix cold-start. 3. ⚠️ **Le rebase de skill-awareness sur develop VA conflicter** : la branche modifie `crates/app-tauri/src/state.rs` et `crates/infrastructure/src/input/mod.rs`, tous deux lourdement réécrits par orchestrator-designation déjà mergé dans develop. Ce n'est **pas un rebase aveugle** : je le ferai avec l'appui de Dev pour résoudre les conflits, puis QA revalide avant mon merge `--no-ff`. 4. **`fix/cold-start-delivery-race` : je recommande sa suppression** (force `-D`, car non mergée), son contenu unique étant préservé dans skill-awareness et le reste n'étant que des commits de release déjà sur main. Je ne l'ai PAS forcée en autonomie (force-delete = action plus difficile à annuler). **Donne-moi le feu vert et je la supprime**, idéalement une fois skill-awareness intégré dans develop. ## État final - Branche courante : **`develop`** @ `9c71a5b` (+9 vs origin, non poussé). - Branches : `develop`, `main` (@ `29232dd`), `feature/agent-skill-awareness`, `feature/agent-skills`, `fix/cold-start-delivery-race`. - Worktree : **propre**. Dis-moi : (a) je lance le prochain chantier sur skill-awareness — confie d'abord à Architect l'arbitrage du chevauchement avec agent-skills ; (b) feu vert ou non pour supprimer `fix/cold-start-delivery-race`. - **Response:** Ménage des branches fait + cadrage du prochain chantier. Aucune action sortante, aucune suppression risquée. ## Action exécutée - **Supprimé `feature/agent-session-limits`** (`git branch -d`, refus auto si non mergée) : 0 commit propre, entièrement intégrée dans develop. Suppression sûre. ## Analyse des 3 branches restantes (relations git vérifiées) - **`feature/agent-skill-awareness`** — base `8452333`, 5 commits propres, NON intégrée. Le chantier le plus riche et récent : `ab34363` (manifeste de skills + outil MCP `idea_skill_read`), `e93a2c1` (fix cold-start delivery), `1a10d67` (test compteur outils MCP 11→12), `566bff4` (brief « capacités IdeA »), `5be8987` (runtime). **À conserver.** - **`feature/agent-skills`** — base `9736c42`, 1 commit `ef101db` (skills domain/usecases/FS store, L12), NON intégrée, **divergente** de skill-awareness (pas ancêtre). ⚠️ Elle touche **les mêmes fichiers** que skill-awareness (`domain/src/skill.rs`, `application/src/skill/*`, `infrastructure/src/store/skill.rs`) → ce sont très probablement **deux itérations concurrentes de la même couche skill**, pas des compléments. **Conservée**, mais l'arbitrage « laquelle fait foi » est une décision d'**architecture, pas git**. - **`fix/cold-start-delivery-race`** — base `6969dc7` (ligne main : elle traîne les commits de release 0.1.0/0.2.0). Son **seul contenu réel** est `9590eac` (fix cold-start), dont le **jumeau logique `e93a2c1` est déjà dans skill-awareness** (message identique). → **doublon superseded**. `git cherry` confirme qu'aucun de ces patchs n'est encore dans develop. ## Décisions / recommandations 1. **Prochain chantier = `feature/agent-skill-awareness`.** Pourquoi : c'est le travail le plus avancé et non intégré, et il englobe le fix cold-start de la branche `fix/cold-start-delivery-race`. 2. **Il DOIT passer par le cycle avant tout merge**, dans cet ordre : - **Architect d'abord** : trancher le chevauchement skill-awareness ⟷ `feature/agent-skills` (couche skill implémentée sur les mêmes fichiers). Décider qui est la source de vérité (probablement skill-awareness subsume agent-skills) avant de figer quoi que ce soit. - **Dev/QA** : revalider vert, en particulier `1a10d67` (compteur d'outils MCP, qui a déjà bougé 11→12 puis 12→13 dans state.rs côté develop) et le fix cold-start. 3. ⚠️ **Le rebase de skill-awareness sur develop VA conflicter** : la branche modifie `crates/app-tauri/src/state.rs` et `crates/infrastructure/src/input/mod.rs`, tous deux lourdement réécrits par orchestrator-designation déjà mergé dans develop. Ce n'est **pas un rebase aveugle** : je le ferai avec l'appui de Dev pour résoudre les conflits, puis QA revalide avant mon merge `--no-ff`. 4. **`fix/cold-start-delivery-race` : je recommande sa suppression** (force `-D`, car non mergée), son contenu unique étant préservé dans skill-awareness et le reste n'étant que des commits de release déjà sur main. Je ne l'ai PAS forcée en autonomie (force-delete = action plus difficile à annuler). **Donne-moi le feu vert et je la supprime**, idéalement une fois skill-awareness intégré dans develop. ## État final - Branche courante : **`develop`** @ `9c71a5b` (+9 vs origin, non poussé). - Branches : `develop`, `main` (@ `29232dd`), `feature/agent-skill-awareness`, `feature/agent-skills`, `fix/cold-start-delivery-race`. - Worktree : **propre**. Dis-moi : (a) je lance le prochain chantier sur skill-awareness — confie d'abord à Architect l'arbitrage du chevauchement avec agent-skills ; (b) feu vert ou non pour supprimer `fix/cold-start-delivery-race`.
- **Prompt:** Main reprend le cycle après cadrage Architect. État connu: repo `/home/anthony/Documents/Projects/IdeA`, branche actuelle `develop`, `develop...origin/develop [ahead 9]`, dirty: `.ideai/conversations/.../handoff.md`, `.ideai/conversations/.../log.jsonl`, `.ideai/layouts.json`, `.ideai/memory/MEMORY.md`, nouveau `.ideai/memory/checkpoint-blocked-until-appimage-030-restart.md`. Architect recommande de ne pas merger la vieille branche `feature/agent-skill-awareness`, mais de créer une branche neuve depuis `develop`, par ex. `feature/agent-skill-awareness-v2`. Merci de décider et effectuer uniquement les actions Git/locales nécessaires: gestion de branche, éventuellement commit/ignore/stash des fichiers runtime si tu le juges nécessaire selon ton rôle. Pas de push. Retourne: branche active, actions effectuées, état `git status --short --branch`, et consignes pour DevBackend/DevFrontend/QA. - **Prompt:** Main reprend le cycle après cadrage Architect. État connu: repo `/home/anthony/Documents/Projects/IdeA`, branche actuelle `develop`, `develop...origin/develop [ahead 9]`, dirty: `.ideai/conversations/.../handoff.md`, `.ideai/conversations/.../log.jsonl`, `.ideai/layouts.json`, `.ideai/memory/MEMORY.md`, nouveau `.ideai/memory/checkpoint-blocked-until-appimage-030-restart.md`. Architect recommande de ne pas merger la vieille branche `feature/agent-skill-awareness`, mais de créer une branche neuve depuis `develop`, par ex. `feature/agent-skill-awareness-v2`. Merci de décider et effectuer uniquement les actions Git/locales nécessaires: gestion de branche, éventuellement commit/ignore/stash des fichiers runtime si tu le juges nécessaire selon ton rôle. Pas de push. Retourne: branche active, actions effectuées, état `git status --short --branch`, et consignes pour DevBackend/DevFrontend/QA.
- **Response:** Décision Git appliquée (local uniquement, aucun push). **Branche active : `feature/agent-skill-awareness-v2`** (créée depuis `develop`, working tree propre). **Actions effectuées :** 1. Les 7 fichiers dirty étaient tous de l'état runtime `.ideai/` (conversations handoff+log, layouts.json, MEMORY.md, nouveau checkpoint-blocked-until-appimage-030-restart.md). Conformément à mon rôle (séparer code de feature et état runtime), je les ai committés sur `develop` : - `c... ` chore(wip): état runtime .ideai (conversations, layouts, mémoire, checkpoint blocage AppImage 0.3.0) 2. Création de la branche neuve `feature/agent-skill-awareness-v2` depuis `develop` (tip = ce commit). J'ai suivi la reco Architect : la vieille `feature/agent-skill-awareness` (5be8987) n'est PAS mergée et reste en place comme archive — repart d'une base develop propre, pas de dette d'historique divergent (develop avait 7 commits d'avance / la feature 5 d'écart). **`git status --short --branch` :** ``` ## feature/agent-skill-awareness-v2 ``` (working tree propre) **Contexte branches :** - `develop` : +9 puis +1 (le commit runtime) vs origin → désormais ahead 10, non poussé. - `feature/agent-skill-awareness` : intacte (5be8987), abandonnée, conservée comme archive. - `feature/agent-skill-awareness-v2` : NOUVELLE base de travail. **Consignes pour DevBackend / DevFrontend / QA :** - Travaillez tous sur **`feature/agent-skill-awareness-v2`**. Ne touchez pas à `develop` ni à la vieille `feature/agent-skill-awareness`. - Ne committez pas vous-mêmes : signalez-moi (via Main) quand l'implémentation est prête, je fais les commits atomiques (code feature séparé de l'état runtime `.ideai/`). - Rappel checkpoint mémoire : un blocage de validation e2e attendait un restart sur AppImage 0.3.0 — vérifier que l'AppImage qui tourne est bien rebuildée avant de conclure une validation (le binaire qui tourne = AppImage, pas les sources). - Quand QA est vert avec sortie réelle, revenez vers moi : je rebase si besoin puis merge `--no-ff` v2 → develop. - **Response:** Décision Git appliquée (local uniquement, aucun push). **Branche active : `feature/agent-skill-awareness-v2`** (créée depuis `develop`, working tree propre). **Actions effectuées :** 1. Les 7 fichiers dirty étaient tous de l'état runtime `.ideai/` (conversations handoff+log, layouts.json, MEMORY.md, nouveau checkpoint-blocked-until-appimage-030-restart.md). Conformément à mon rôle (séparer code de feature et état runtime), je les ai committés sur `develop` : - `c... ` chore(wip): état runtime .ideai (conversations, layouts, mémoire, checkpoint blocage AppImage 0.3.0) 2. Création de la branche neuve `feature/agent-skill-awareness-v2` depuis `develop` (tip = ce commit). J'ai suivi la reco Architect : la vieille `feature/agent-skill-awareness` (5be8987) n'est PAS mergée et reste en place comme archive — repart d'une base develop propre, pas de dette d'historique divergent (develop avait 7 commits d'avance / la feature 5 d'écart). **`git status --short --branch` :** ``` ## feature/agent-skill-awareness-v2 ``` (working tree propre) **Contexte branches :** - `develop` : +9 puis +1 (le commit runtime) vs origin → désormais ahead 10, non poussé. - `feature/agent-skill-awareness` : intacte (5be8987), abandonnée, conservée comme archive. - `feature/agent-skill-awareness-v2` : NOUVELLE base de travail. **Consignes pour DevBackend / DevFrontend / QA :** - Travaillez tous sur **`feature/agent-skill-awareness-v2`**. Ne touchez pas à `develop` ni à la vieille `feature/agent-skill-awareness`. - Ne committez pas vous-mêmes : signalez-moi (via Main) quand l'implémentation est prête, je fais les commits atomiques (code feature séparé de l'état runtime `.ideai/`). - Rappel checkpoint mémoire : un blocage de validation e2e attendait un restart sur AppImage 0.3.0 — vérifier que l'AppImage qui tourne est bien rebuildée avant de conclure une validation (le binaire qui tourne = AppImage, pas les sources). - Quand QA est vert avec sortie réelle, revenez vers moi : je rebase si besoin puis merge `--no-ff` v2 → develop.
@ -24,3 +22,5 @@ objective: Nouvelle feature cadrée par Architect, prête à démarrer le dével
- **Prompt:** QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet. Changements code attendus: Backend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests. Frontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests. Runtime dirty `.ideai/*` aussi présent suite aux conversations live. Stratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status. - **Prompt:** QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet. Changements code attendus: Backend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests. Frontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests. Runtime dirty `.ideai/*` aussi présent suite aux conversations live. Stratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status.
- **Response:** Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée. ## Commits (sur la feature) **1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)** - Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs` **2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)** - Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx) **3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)** - Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6) ## Exclus / hors commit Rien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code. ## Décision merge : MERGÉ Lot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct). - Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54. ## État final - **Branche courante : `develop`** (tip `3bfb932`). - `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme). - Working tree **propre**. - Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`). - Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages. Dis-moi quand le Lot B est cadré : je prépare la branche. - **Response:** Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée. ## Commits (sur la feature) **1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)** - Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs` **2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)** - Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx) **3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)** - Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6) ## Exclus / hors commit Rien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code. ## Décision merge : MERGÉ Lot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct). - Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54. ## État final - **Branche courante : `develop`** (tip `3bfb932`). - `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme). - Working tree **propre**. - Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`). - Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages. Dis-moi quand le Lot B est cadré : je prépare la branche.
- **Prompt:** Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état. - **Prompt:** Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état.
- **Response:** Repo prêt pour le Lot B « workstate delegation/queue snapshot ». ## Actions 1. Dirty runtime live (2 conversations `.ideai/conversations/*`, append-only) figé sur `develop` sans perte : - `7453181` — chore(wip): état runtime .ideai (flux conversation live). 2. Nouvelle branche feature créée depuis `develop` propre : - `git checkout -b feature/workstate-delegation-queue` (depuis `develop` @ `7453181`, = `3bfb932` + chore runtime). ## État final - **Branche courante : `feature/workstate-delegation-queue`** (tip `7453181`). - Base : `develop`, historique linéaire, working tree **propre**. - `develop` ahead origin/develop de **22** (local only). Aucune branche obsolète supprimée. **Aucun push.** DevBackend/DevFrontend peuvent implémenter le Lot B. Je reprends la main à l'implémentation terminée (tests verts) pour les commits atomiques (code séparé du runtime) et la décision de merge.
- **Prompt:** Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push. Contexte: - Branche active attendue: `feature/workstate-delegation-queue`. - QA vert: - `cargo fmt --all -- --check` OK - `cargo test -p infrastructure mailbox --lib` OK (13 passed) - `cargo test -p application --test workstate` OK (12 passed) - `cargo test -p app-tauri --test dto_agents` OK (20 passed) - `cargo check -p app-tauri` OK - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests) - `cd frontend && npx tsc --noEmit` OK - `cargo test -p domain mailbox --lib` OK (6 passed) - Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`. - Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`. - Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code. Décide: 1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire), 2. merge local éventuel vers `develop`, 3. état final. Ne push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final.

View File

@ -47,3 +47,5 @@
{"id":"93ed87f6-8dc6-4ff8-85d9-81aa30d30bed","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971560322,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet.\n\nChangements code attendus:\nBackend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests.\nFrontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests.\nRuntime dirty `.ideai/*` aussi présent suite aux conversations live.\n\nStratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status."} {"id":"93ed87f6-8dc6-4ff8-85d9-81aa30d30bed","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971560322,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA verte pour `feature/conversation-ux-live-state` Lot A, réserve environnementale non bloquante socket Unix. Merci de committer localement de façon atomique puis décider le merge local vers `develop` selon les règles projet.\n\nChangements code attendus:\nBackend/Tauri: `crates/application/src/workstate/`, `crates/application/tests/workstate.rs`, exports terminal/lib, snapshots live, Tauri DTO/command/state/lib/tests.\nFrontend: domain/ports/adapters/mock/workState, feature workstate, ProjectsView/tests.\nRuntime dirty `.ideai/*` aussi présent suite aux conversations live.\n\nStratégie demandée: séparer code backend/frontend si pertinent, séparer runtime `.ideai` en chore wip si tu le gardes, ne pas push, ne pas supprimer les branches obsolètes. Après commit, décide toi-même merge ou différer; si merge, fais-le localement. Retourne hashes, messages, fichiers inclus/exclus, branche finale et status."}
{"id":"d93a5849-41e6-4249-9dbf-9691c760c64b","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971634198,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée.\n\n## Commits (sur la feature)\n**1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)**\n- Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs`\n\n**2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)**\n- Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx)\n\n**3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)**\n- Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6)\n\n## Exclus / hors commit\nRien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code.\n\n## Décision merge : MERGÉ\nLot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct).\n- Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54.\n\n## État final\n- **Branche courante : `develop`** (tip `3bfb932`).\n- `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme).\n- Working tree **propre**.\n- Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`).\n- Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages.\n\nDis-moi quand le Lot B est cadré : je prépare la branche."} {"id":"d93a5849-41e6-4249-9dbf-9691c760c64b","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971634198,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Lot A committé atomiquement (code backend / frontend séparés + runtime à part) **et mergé** dans `develop`. Aucun push, aucune branche supprimée.\n\n## Commits (sur la feature)\n**1. `aae1849` — feat(workstate): read-model live-state minimal (Lot A backend)**\n- Inclus : `crates/application/src/workstate/mod.rs` (nouv.), `crates/application/tests/workstate.rs` (nouv.), `crates/application/src/lib.rs`, `crates/application/src/terminal/{mod,registry}.rs`, `crates/app-tauri/src/{commands,dto,lib,state}.rs`, `crates/app-tauri/tests/dto_agents.rs`\n\n**2. `17685a0` — feat(workstate): UI live-state (Lot A frontend)**\n- Inclus : `frontend/src/adapters/{index,workState}.ts` (workState nouv.), `frontend/src/adapters/mock/index.ts`, `frontend/src/domain/index.ts`, `frontend/src/ports/index.ts`, `frontend/src/features/projects/{ProjectsView.tsx,projects.test.tsx}`, `frontend/src/features/workstate/` (nouv. : index.ts, ProjectWorkStatePanel.tsx, useProjectWorkState.ts, workstate.test.tsx)\n\n**3. `a06328a` — chore(wip): état runtime .ideai (conversations live, agents, layouts)**\n- Inclus : `.ideai/agents.json`, `.ideai/layouts.json`, `.ideai/conversations/*` (handoff + log.jsonl ×6)\n\n## Exclus / hors commit\nRien laissé de côté : working tree **propre**. Le runtime `.ideai` est gardé (cohérent avec l'historique) mais isolé dans le chore — jamais mélangé au code.\n\n## Décision merge : MERGÉ\nLot A est vert, atomique et autonome (un read-model + son UI) → je l'intègre tout de suite pour limiter la dérive de `develop`, plutôt que d'attendre les lots suivants. Pas de rebase nécessaire (develop ancêtre direct).\n- Merge `--no-ff` : **`3bfb932`** — merge(workstate): intègre le read-model live-state + UX conversations (Lot A) dans develop. Sans conflit (ort), 35 fichiers / +1146 -54.\n\n## État final\n- **Branche courante : `develop`** (tip `3bfb932`).\n- `develop` : **ahead origin/develop de 21** (local only, aucun push — conforme).\n- Working tree **propre**.\n- Branche `feature/conversation-ux-live-state` **conservée** (tip `a06328a`, intégrée). Disponible pour les lots suivants ; pour le **Lot B**, je rebascule/réaligne une branche feature depuis `develop` au prochain ticket (pas de dev direct sur `develop`).\n- Réserve QA (socket Unix non bindable en sandbox, alternatives skips vertes) consignée dans les messages.\n\nDis-moi quand le Lot B est cadré : je prépare la branche."}
{"id":"95e2a7a2-bb92-474f-83ec-0a2a7bab2c3e","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971838757,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état."} {"id":"95e2a7a2-bb92-474f-83ec-0a2a7bab2c3e","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971838757,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Préparation repo pour Lot B `workstate delegation/queue snapshot` après merge local Lot A dans `develop` tip `3bfb932`. Merci de créer/basculer sur une branche feature propre depuis develop, gérer tout dirty runtime éventuel sans perte, ne push pas, ne supprime pas branches obsolètes. Retourne branche courante + état."}
{"id":"9654549c-fd9d-4661-8dc2-e6d2a78eab7a","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781971855209,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Repo prêt pour le Lot B « workstate delegation/queue snapshot ».\n\n## Actions\n1. Dirty runtime live (2 conversations `.ideai/conversations/*`, append-only) figé sur `develop` sans perte :\n - `7453181` — chore(wip): état runtime .ideai (flux conversation live).\n2. Nouvelle branche feature créée depuis `develop` propre :\n - `git checkout -b feature/workstate-delegation-queue` (depuis `develop` @ `7453181`, = `3bfb932` + chore runtime).\n\n## État final\n- **Branche courante : `feature/workstate-delegation-queue`** (tip `7453181`).\n- Base : `develop`, historique linéaire, working tree **propre**.\n- `develop` ahead origin/develop de **22** (local only). Aucune branche obsolète supprimée. **Aucun push.**\n\nDevBackend/DevFrontend peuvent implémenter le Lot B. Je reprends la main à l'implémentation terminée (tests verts) pour les commits atomiques (code séparé du runtime) et la décision de merge."}
{"id":"53773ad0-5bb6-4e24-8456-d7ab17be3db0","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781974288762,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push.\n\nContexte:\n- Branche active attendue: `feature/workstate-delegation-queue`.\n- QA vert:\n - `cargo fmt --all -- --check` OK\n - `cargo test -p infrastructure mailbox --lib` OK (13 passed)\n - `cargo test -p application --test workstate` OK (12 passed)\n - `cargo test -p app-tauri --test dto_agents` OK (20 passed)\n - `cargo check -p app-tauri` OK\n - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests)\n - `cd frontend && npx tsc --noEmit` OK\n - `cargo test -p domain mailbox --lib` OK (6 passed)\n- Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`.\n- Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`.\n- Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code.\n\nDécide:\n1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire),\n2. merge local éventuel vers `develop`,\n3. état final.\n\nNe push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final."}

File diff suppressed because one or more lines are too long

View File

@ -69,3 +69,6 @@
{"id":"00aa406e-b715-4f9b-9978-f52b9a37e27f","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781945759965,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Diagnostic : blocage environnement/sandbox, pas régression applicative liée au MVP `agent-skill-awareness-v2` ni aux changements récents.\n\nConstats :\n- `cargo test -p app-tauri bind_endpoint -- --nocapture` échoue exactement comme QA : `lay corpse socket: Os { code: 1, kind: PermissionDenied, message: \"Operation not permitted\" }` sur `std::os::unix::net::UnixListener::bind`.\n- `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` échoue car `bind_endpoint` retourne `None` pour `/run/user/1000/idea-mcp/<project>.sock`.\n- Même en forçant `XDG_RUNTIME_DIR=/tmp`, les tests loopback échouent : `bind_endpoint returned None for \"/tmp/idea-mcp/<project>.sock\"`.\n- Smoke test Rust minimal hors repo, compilé dans `/tmp`, confirme que le sandbox interdit `AF_UNIX` path sockets partout :\n - `/tmp/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted`\n - `/run/user/1000/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted`\n\nInterprétation :\n- Les tests qui échouent nécessitent un vrai listener Unix domain socket. Léchec se produit avant la logique IdeA, au niveau syscall de bind socket.\n- Le code récent `agent-skill-awareness-v2` ne touche pas `app-tauri`, `state.rs`, `mcp_endpoint.rs`, `mcp_bridge.rs`, ni la dérivation/binding des endpoints.\n- `mcp_endpoint` choisit correctement `$XDG_RUNTIME_DIR` puis `/tmp`; le fait que `/tmp` échoue aussi exclut un simple problème de permissions de `/run/user/1000`.\n\nCommandes de vérification exécutées :\n- `cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue `PermissionDenied`, reproduit QA.\n- `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue `bind_endpoint returned None`, reproduit QA.\n- `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue aussi `PermissionDenied`.\n- `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue aussi `bind_endpoint returned None`.\n- Smoke Rust minimal avec `UnixListener::bind` dans `/tmp` et `/run/user/1000` => échoue `PermissionDenied` dans les deux cas.\n\nCommandes alternatives vertes dans ce sandbox, en excluant uniquement les tests qui nécessitent un vrai socket Unix :\n```text\ncargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together\n```\nRésultat : OK, 9 passed, 4 filtered out.\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat : OK, 39 passed, 8 filtered out.\n\nAucune correction code/test appliquée. Je nai pas touché aux fichiers runtime `.ideai`."} {"id":"00aa406e-b715-4f9b-9978-f52b9a37e27f","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781945759965,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Diagnostic : blocage environnement/sandbox, pas régression applicative liée au MVP `agent-skill-awareness-v2` ni aux changements récents.\n\nConstats :\n- `cargo test -p app-tauri bind_endpoint -- --nocapture` échoue exactement comme QA : `lay corpse socket: Os { code: 1, kind: PermissionDenied, message: \"Operation not permitted\" }` sur `std::os::unix::net::UnixListener::bind`.\n- `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` échoue car `bind_endpoint` retourne `None` pour `/run/user/1000/idea-mcp/<project>.sock`.\n- Même en forçant `XDG_RUNTIME_DIR=/tmp`, les tests loopback échouent : `bind_endpoint returned None for \"/tmp/idea-mcp/<project>.sock\"`.\n- Smoke test Rust minimal hors repo, compilé dans `/tmp`, confirme que le sandbox interdit `AF_UNIX` path sockets partout :\n - `/tmp/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted`\n - `/run/user/1000/idea-socket-smoke-*.sock` => `PermissionDenied / Operation not permitted`\n\nInterprétation :\n- Les tests qui échouent nécessitent un vrai listener Unix domain socket. Léchec se produit avant la logique IdeA, au niveau syscall de bind socket.\n- Le code récent `agent-skill-awareness-v2` ne touche pas `app-tauri`, `state.rs`, `mcp_endpoint.rs`, `mcp_bridge.rs`, ni la dérivation/binding des endpoints.\n- `mcp_endpoint` choisit correctement `$XDG_RUNTIME_DIR` puis `/tmp`; le fait que `/tmp` échoue aussi exclut un simple problème de permissions de `/run/user/1000`.\n\nCommandes de vérification exécutées :\n- `cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue `PermissionDenied`, reproduit QA.\n- `cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue `bind_endpoint returned None`, reproduit QA.\n- `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri bind_endpoint -- --nocapture` => échoue aussi `PermissionDenied`.\n- `XDG_RUNTIME_DIR=/tmp cargo test -p app-tauri mcp_e2e_loopback_tests -- --nocapture` => échoue aussi `bind_endpoint returned None`.\n- Smoke Rust minimal avec `UnixListener::bind` dans `/tmp` et `/run/user/1000` => échoue `PermissionDenied` dans les deux cas.\n\nCommandes alternatives vertes dans ce sandbox, en excluant uniquement les tests qui nécessitent un vrai socket Unix :\n```text\ncargo test -p app-tauri --test orchestrator_wiring -- --skip open_binds_the_project_loopback_endpoint --skip double_open_keeps_a_single_endpoint_no_address_in_use --skip close_cleans_up_the_endpoint_socket_file --skip file_watcher_and_loopback_endpoint_live_together\n```\nRésultat : OK, 9 passed, 4 filtered out.\n\n```text\ncargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback\n```\nRésultat : OK, 39 passed, 8 filtered out.\n\nAucune correction code/test appliquée. Je nai pas touché aux fichiers runtime `.ideai`."}
{"id":"bd1d1082-e11d-4754-a83f-ba89233b25e4","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781946742079,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot A backend/Tauri du chantier `UX conversations/délégations + read-model live-state minimal` sur la branche `feature/conversation-ux-live-state`. Respecte le cadrage Architect: read-only, sans nouvelle persistance durable, pas de lecture log/handoff, pas de FIFO complète.\n\nObjectif backend:\n- Ajouter un read-model `ProjectWorkState` qui liste les agents du manifeste avec leur état live et busy.\n- Agréger depuis les stores/registres existants: manifeste agents via context store, `LiveSessions`/registries existants, `InputMediator::busy_state`.\n- Conserver l'ordre manifeste.\n- Ignorer les agents live absents du manifeste.\n- Exposer une commande Tauri `get_project_work_state(projectId)`.\n\nContrats recommandés par Architect:\n- `ProjectWorkState { agents: Vec<AgentWorkState> }`\n- `AgentWorkState { agent_id, name, profile_id, live: Option<LiveWorkSession>, busy }`\n- `LiveWorkSession { node_id, session_id, kind: Pty|Structured }` si raisonnable; sinon explique pourquoi tu omets `kind`.\n- Busy DTO camelCase/tag `state`: `idle` ou `busy { ticket, sinceMs }` selon les types existants.\n\nFichiers probables: `crates/application/src/workstate/mod.rs`, `crates/application/src/lib.rs`, peut-être `crates/application/src/terminal/registry.rs`; Tauri `dto.rs`, `commands.rs`, `state.rs`, `lib.rs`.\n\nTests attendus:\n- tests application `workstate`: aucun live/busy; live PTY ou structured; busy; agent live hors manifeste ignoré.\n- tests DTO/commande si conventions existantes.\n- Exécute au minimum `cargo test -p application workstate`, `cargo check -p app-tauri`, et les tests app-tauri pertinents que tu peux lancer dans ce sandbox. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques."} {"id":"bd1d1082-e11d-4754-a83f-ba89233b25e4","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781946742079,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot A backend/Tauri du chantier `UX conversations/délégations + read-model live-state minimal` sur la branche `feature/conversation-ux-live-state`. Respecte le cadrage Architect: read-only, sans nouvelle persistance durable, pas de lecture log/handoff, pas de FIFO complète.\n\nObjectif backend:\n- Ajouter un read-model `ProjectWorkState` qui liste les agents du manifeste avec leur état live et busy.\n- Agréger depuis les stores/registres existants: manifeste agents via context store, `LiveSessions`/registries existants, `InputMediator::busy_state`.\n- Conserver l'ordre manifeste.\n- Ignorer les agents live absents du manifeste.\n- Exposer une commande Tauri `get_project_work_state(projectId)`.\n\nContrats recommandés par Architect:\n- `ProjectWorkState { agents: Vec<AgentWorkState> }`\n- `AgentWorkState { agent_id, name, profile_id, live: Option<LiveWorkSession>, busy }`\n- `LiveWorkSession { node_id, session_id, kind: Pty|Structured }` si raisonnable; sinon explique pourquoi tu omets `kind`.\n- Busy DTO camelCase/tag `state`: `idle` ou `busy { ticket, sinceMs }` selon les types existants.\n\nFichiers probables: `crates/application/src/workstate/mod.rs`, `crates/application/src/lib.rs`, peut-être `crates/application/src/terminal/registry.rs`; Tauri `dto.rs`, `commands.rs`, `state.rs`, `lib.rs`.\n\nTests attendus:\n- tests application `workstate`: aucun live/busy; live PTY ou structured; busy; agent live hors manifeste ignoré.\n- tests DTO/commande si conventions existantes.\n- Exécute au minimum `cargo test -p application workstate`, `cargo check -p app-tauri`, et les tests app-tauri pertinents que tu peux lancer dans ce sandbox. Ne committe pas. Réponds avec fichiers modifiés, commandes/résultats, et risques."}
{"id":"5d78a64e-ce73-4fca-b62d-7af302dbac90","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781971233449,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Lot A backend/Tauri implémenté sur `feature/conversation-ux-live-state`, sans commit.\n\nFichiers modifiés pour le lot :\n- `crates/application/src/workstate/mod.rs` (nouveau) : read-model `GetProjectWorkState`, `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`.\n- `crates/application/src/lib.rs` : exports du module workstate et des snapshots live typés.\n- `crates/application/src/terminal/mod.rs` : exports `LiveSessionKind` / `LiveSessionSnapshot`.\n- `crates/application/src/terminal/registry.rs` : ajout de `LiveSessionKind`, `LiveSessionSnapshot`, `LiveSessions::live_agent_snapshots()` ; `live_agents()` existant inchangé.\n- `crates/application/tests/workstate.rs` (nouveau) : tests du read-model.\n- `crates/app-tauri/src/state.rs` : construction/câblage du use case dans `AppState` avec `AgentContextStore`, `LiveSessions`, `InputMediator`.\n- `crates/app-tauri/src/commands.rs` : commande `get_project_work_state(projectId)`.\n- `crates/app-tauri/src/dto.rs` : DTO `ProjectWorkStateDto`, `AgentWorkStateDto`, `LiveWorkSessionDto`, `LiveWorkSessionKindDto` ; `busy` réutilise `domain::AgentBusyState` (`state: idle|busy`, `sinceMs`).\n- `crates/app-tauri/src/lib.rs` : enregistrement Tauri command.\n- `crates/app-tauri/tests/dto_agents.rs` : test DTO camelCase/live/busy.\n\nComportement :\n- Read-only, aucune nouvelle persistance durable.\n- Aucune lecture log/handoff, aucune FIFO complète exposée.\n- Agents listés dans lordre du manifeste.\n- Agents live absents du manifeste ignorés.\n- Live session expose `nodeId`, `sessionId`, `kind: pty|structured`.\n\nCommandes exécutées côté DevBackend :\n- `cargo fmt -p application -p app-tauri` : OK.\n- `cargo test -p application workstate` : OK, 5 tests workstate passés.\n- `cargo check -p app-tauri` : OK.\n- `cargo test -p app-tauri --test dto_agents project_work_state -- --nocapture` : OK, 1 passed.\n- `cargo test -p app-tauri --test dto_agents` : OK, 19 passed.\n- `cargo test -p app-tauri --test list_live_agents_r0b` : OK, 5 passed.\n- `cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback` : OK, 39 passed, 8 filtered out.\n- `cargo test -p application` : OK, application complète verte. Warning existant/non lié : `CapturingFs::writes` unused dans `crates/application/tests/orchestrator_service.rs:1642`.\n\nValidation QA : vert. QA a relu le périmètre, confirmé read-only / ordre manifeste / absence log-handoff-persistance, et réexécuté les commandes clés avec succès. Même réserve non bloquante que précédemment : les 8 tests app-tauri filtrés exigent un vrai socket Unix loopback, bloqué dans ce sandbox par `UnixListener::bind` `PermissionDenied`; à exécuter hors sandbox permissif socket Unix.\n\nRisques / notes :\n- `kind` est inclus car lagrégateur sait distinguer PTY vs structured via les deux registres.\n- Le read-model retourne un instantané live/busy au moment de lappel ; pas de garantie transactionnelle entre registre live et busy state, ce qui est cohérent avec un read-model live minimal.\n- `git status` montre aussi des fichiers `.ideai/*` runtime modifiés par lorchestration/conversation ; je ne les ai pas édités pour ce lot et ils ne font pas partie du livrable."} {"id":"5d78a64e-ce73-4fca-b62d-7af302dbac90","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781971233449,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Lot A backend/Tauri implémenté sur `feature/conversation-ux-live-state`, sans commit.\n\nFichiers modifiés pour le lot :\n- `crates/application/src/workstate/mod.rs` (nouveau) : read-model `GetProjectWorkState`, `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`.\n- `crates/application/src/lib.rs` : exports du module workstate et des snapshots live typés.\n- `crates/application/src/terminal/mod.rs` : exports `LiveSessionKind` / `LiveSessionSnapshot`.\n- `crates/application/src/terminal/registry.rs` : ajout de `LiveSessionKind`, `LiveSessionSnapshot`, `LiveSessions::live_agent_snapshots()` ; `live_agents()` existant inchangé.\n- `crates/application/tests/workstate.rs` (nouveau) : tests du read-model.\n- `crates/app-tauri/src/state.rs` : construction/câblage du use case dans `AppState` avec `AgentContextStore`, `LiveSessions`, `InputMediator`.\n- `crates/app-tauri/src/commands.rs` : commande `get_project_work_state(projectId)`.\n- `crates/app-tauri/src/dto.rs` : DTO `ProjectWorkStateDto`, `AgentWorkStateDto`, `LiveWorkSessionDto`, `LiveWorkSessionKindDto` ; `busy` réutilise `domain::AgentBusyState` (`state: idle|busy`, `sinceMs`).\n- `crates/app-tauri/src/lib.rs` : enregistrement Tauri command.\n- `crates/app-tauri/tests/dto_agents.rs` : test DTO camelCase/live/busy.\n\nComportement :\n- Read-only, aucune nouvelle persistance durable.\n- Aucune lecture log/handoff, aucune FIFO complète exposée.\n- Agents listés dans lordre du manifeste.\n- Agents live absents du manifeste ignorés.\n- Live session expose `nodeId`, `sessionId`, `kind: pty|structured`.\n\nCommandes exécutées côté DevBackend :\n- `cargo fmt -p application -p app-tauri` : OK.\n- `cargo test -p application workstate` : OK, 5 tests workstate passés.\n- `cargo check -p app-tauri` : OK.\n- `cargo test -p app-tauri --test dto_agents project_work_state -- --nocapture` : OK, 1 passed.\n- `cargo test -p app-tauri --test dto_agents` : OK, 19 passed.\n- `cargo test -p app-tauri --test list_live_agents_r0b` : OK, 5 passed.\n- `cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback` : OK, 39 passed, 8 filtered out.\n- `cargo test -p application` : OK, application complète verte. Warning existant/non lié : `CapturingFs::writes` unused dans `crates/application/tests/orchestrator_service.rs:1642`.\n\nValidation QA : vert. QA a relu le périmètre, confirmé read-only / ordre manifeste / absence log-handoff-persistance, et réexécuté les commandes clés avec succès. Même réserve non bloquante que précédemment : les 8 tests app-tauri filtrés exigent un vrai socket Unix loopback, bloqué dans ce sandbox par `UnixListener::bind` `PermissionDenied`; à exécuter hors sandbox permissif socket Unix.\n\nRisques / notes :\n- `kind` est inclus car lagrégateur sait distinguer PTY vs structured via les deux registres.\n- Le read-model retourne un instantané live/busy au moment de lappel ; pas de garantie transactionnelle entre registre live et busy state, ce qui est cohérent avec un read-model live minimal.\n- `git status` montre aussi des fichiers `.ideai/*` runtime modifiés par lorchestration/conversation ; je ne les ai pas édités pour ce lot et ils ne font pas partie du livrable."}
{"id":"7b0a1b7a-bfad-4d97-89f8-f4bb9b32740d","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781971869030,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot B backend/Tauri `workstate delegation/queue snapshot` sur la branche `feature/workstate-delegation-queue`. Respecte le cadrage Architect.\n\nObjectif:\n- Ajouter un port read-only `AgentQueueSnapshot` dans `domain/src/mailbox.rs` avec snapshots clonés des tickets, sans exposer ni cloner les senders.\n- Implémenter ce port pour `InMemoryMailbox` en conservant l'ordre FIFO et positions recalculées.\n- Étendre `GetProjectWorkState` pour inclure `agents[].tickets`, en croisant snapshot queue + `InputMediator::busy_state` pour dériver `inProgress` vs `queued`.\n- Conserver manifest boundary: tickets d'agents absents du manifeste ignorés.\n- Générer `taskPreview` côté application: trim, espaces normalisés, coupure ~160 caractères; conserver `taskLen` original utile.\n- Exposer DTO Tauri camelCase dans la commande existante `get_project_work_state`.\n\nDécision de périmètre: afficher les tickets `source == Agent` en priorité pour délégations; si tu inclus aussi human, explique et teste le libellé/source. Ne change pas la sémantique enqueue/resolve/cancel. Pas de nouvelle persistance, pas lecture log/handoff, pas événement queue changé.\n\nTests attendus:\n- infrastructure mailbox snapshot: ordre/metadata/read-only/update after resolve/cancel.\n- application workstate: queue vide, deux tickets ordonnés, busy first inProgress, idle queued, absent manifest ignored, preview truncation, source mapping.\n- app-tauri DTO serialization tickets.\n\nCommandes: `cargo fmt`, `cargo test -p infrastructure mailbox --lib`, `cargo test -p application --test workstate`, `cargo test -p app-tauri --test dto_agents`, `cargo check -p app-tauri`, et autres pertinentes. Ne committe pas. Ignore les fichiers runtime `.ideai/*` dirty, ne les modifie pas volontairement."}
{"id":"2c25d734-281e-4f2a-850e-684fec6e0ba7","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781973784611,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Reprise orchestration Main. Je retrouve la branche `feature/workstate-delegation-queue` avec des modifications backend/Tauri non committées pour le Lot B `workstate delegation/queue snapshot` (ticket mentionné par l'utilisateur: dbca904a-2843-4f41-9926-d13b7804c70f). Peux-tu confirmer ton état exact: périmètre implémenté, choix notables (notamment tickets human inclus ou exclus), commandes exécutées et résultats réels, et ce qu'il reste selon toi ? Ne modifie rien, ne committe pas."}
{"id":"b0d07b39-565c-4d30-a9ae-0c3721338aeb","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781973900817,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"État DevBackend — branche `feature/workstate-delegation-queue`, Lot B « workstate delegation/queue snapshot » (ticket dbca904a). **Rien committé, rien modifié pendant ce diagnostic.**\n\n## Périmètre implémenté\nExtension du read-model live-state (Lot A) pour exposer, **par agent**, ses tickets de délégation en file FIFO. 4 couches touchées (sens hexagonal respecté) :\n\n- **domain/mailbox.rs** : nouveau value object `QueuedTicketSnapshot` (données du ticket + `position`, 0 = tête ; **jamais** le `oneshot::Sender`) + nouveau **port lecture seule** `AgentQueueSnapshot` (`queue_for(agent) -> Vec<QueuedTicketSnapshot>`), **ségrégé (ISP)** du port mutant `AgentMailbox`. Re-export depuis `domain/lib.rs`.\n- **infrastructure/mailbox/mod.rs** : `impl AgentQueueSnapshot for InMemoryMailbox` — clone les données sous le lock, recalcule la position depuis l'ordre courant, lecture pure (n'altère pas la file).\n- **application/workstate/mod.rs** : nouveaux `AgentTicketState`, `TicketWorkStatus` (InProgress/Queued), `TicketWorkSource` (Human/Agent), `task_preview` (160 car., espaces normalisés) + `task_len`. `GetProjectWorkState` gagne une dépendance `queue: Arc<dyn AgentQueueSnapshot>` ; champ `AgentWorkState.tickets` peuplé.\n- **app-tauri/dto.rs** : `AgentTicketStateDto`, `TicketWorkStatusDto`, `TicketWorkSourceDto` (camelCase, enum source taggée `kind`), champ `AgentWorkStateDto.tickets`.\n- **app-tauri/state.rs** : composition root — même `InMemoryMailbox` concret partagé en **deux vues Arc** (`AgentMailbox` mutant + `AgentQueueSnapshot` lecture), injecté dans `GetProjectWorkState`.\n\n## Choix notables\n- **Tickets human INCLUS** (pas de filtrage par source). `TicketWorkSource::Human`/`Agent{agent_id}` mappé dans les deux sens, couvert par le test `workstate_maps_human_and_agent_ticket_sources`. La file liste tous les tickets quelle que soit l'origine.\n- **Statut dérivé, jamais stocké** : InProgress = ticket dont l'id == ticket busy courant de l'agent ; les autres Queued ; agent idle → tout Queued.\n- **ISP** : port lecture distinct plutôt que d'élargir `AgentMailbox` ; observer la file ne la mute jamais (test dédié).\n- **task_preview borné** à 160 car., texte intégral jamais envoyé à l'UI, longueur réelle via `task_len`.\n- **Frontière manifest préservée** : seuls les agents du manifest reçoivent des tickets ; file d'un agent absent du manifest ignorée (test dédié).\n\n## Commandes exécutées (à l'instant) + résultats réels\n`cargo test -p domain -p application -p infrastructure -p app-tauri` → **tout vert, 0 échec**.\n- domain : 176 unit (+1 nouveau `queued_ticket_snapshot_…`)\n- infrastructure : 228 unit (+6 nouveaux `snapshot_*` : empty/fifo-order/metadata/read-only/après resolve/après cancel)\n- application : 43 unit + `workstate.rs` **12** (+7 nouveaux : sans-queue, fifo, busy→in-progress, idle→tout-queued, agent-hors-manifest, troncature preview, sources human+agent)\n- app-tauri : lib 47 + `dto_agents.rs` (+1 `project_work_state_dto_serialises_tickets_camelcase`)\nAucun warning bloquant.\n\n## Ce qu'il reste (selon moi)\n1. **Frontend (périmètre DevFrontend)** : aucune consommation TS/React des nouveaux champs DTO `tickets` — rendu du panneau de délégations non fait.\n2. **Commit/merge (périmètre Git)** : 14 fichiers non commités ; le code backend est prêt à committer. À noter : les fichiers `.ideai/` modifiés (conversations/handoff/log/layouts) sont du **bruit runtime**, pas la feature.\n3. **Validation e2e/AppImage** : non faite (binaire qui tourne = AppImage, pas les sources).\n4. **Cadrage Architect** : si tu veux une validation formelle du nouveau port `AgentQueueSnapshot` dans `architect.md`, je l'ai conçu en cohérence ISP mais ce n'est pas tranché dans la cartographie.\n\nEn résumé : backend Lot B **complet et vert** au niveau tests unitaires/intégration ; restent le front, le commit Git et la validation e2e."}

View File

@ -18,8 +18,9 @@
"type": "leaf", "type": "leaf",
"node": { "node": {
"id": "d4b8c0d1-a44a-4c45-bbe9-26991f79b465", "id": "d4b8c0d1-a44a-4c45-bbe9-26991f79b465",
"session": "d5e00a5c-591e-4061-9104-c9ea7c9e01c3", "session": "813f17c2-d056-4875-b9a0-3c18e4e40776",
"agent": "a6ced819-b893-4213-b003-9e9dc79b9641" "agent": "a6ced819-b893-4213-b003-9e9dc79b9641",
"agentWasRunning": true
} }
}, },
"weight": 1.0 "weight": 1.0
@ -36,8 +37,9 @@
"type": "leaf", "type": "leaf",
"node": { "node": {
"id": "71564af2-673a-46c7-a0c0-877f89f6e49e", "id": "71564af2-673a-46c7-a0c0-877f89f6e49e",
"session": "cadc1432-ccc3-4849-94bb-5cf342a85fe1", "session": "384778a6-216f-47ed-90f0-67de6812b59d",
"agent": "aefdbd61-e3d4-4bc1-9f42-c259446a97b5" "agent": "73c853d1-c0fd-463b-ad17-1d24fefa371f",
"agentWasRunning": true
} }
}, },
"weight": 1.0 "weight": 1.0
@ -47,7 +49,9 @@
"type": "leaf", "type": "leaf",
"node": { "node": {
"id": "8529e97f-ce06-490c-b3d0-531c3dfee442", "id": "8529e97f-ce06-490c-b3d0-531c3dfee442",
"session": "b7e0ab21-25bf-4158-8fcc-e2c67d49868e" "session": "fac01ad4-0e99-4b2a-bdec-621493cf9c57",
"agent": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5",
"agentWasRunning": true
} }
}, },
"weight": 1.0 "weight": 1.0
@ -61,6 +65,19 @@
} }
} }
} }
},
{
"id": "74489208-01a7-4005-aba4-b9c41ab843fd",
"name": "Git Graph",
"kind": "gitGraph",
"tree": {
"root": {
"type": "leaf",
"node": {
"id": "2396a80e-bc98-47cd-b711-c7cff0010bb6"
}
}
}
} }
] ]
} }

View File

@ -9,8 +9,9 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use application::{ use application::{
AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport, AgentTicketState, AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput,
LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput, ProjectWorkState, HealthInput, HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput,
ProjectWorkState, TicketWorkSource, TicketWorkStatus,
}; };
use domain::{AgentBusyState, Project, ProjectId}; use domain::{AgentBusyState, Project, ProjectId};
@ -1503,6 +1504,87 @@ pub struct LiveWorkSessionDto {
pub kind: LiveWorkSessionKindDto, pub kind: LiveWorkSessionKindDto,
} }
/// Derived processing status of a queued ticket.
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum TicketWorkStatusDto {
/// The agent's current busy turn is running this ticket.
InProgress,
/// Waiting behind the head / the agent is idle.
Queued,
}
impl From<TicketWorkStatus> for TicketWorkStatusDto {
fn from(status: TicketWorkStatus) -> Self {
match status {
TicketWorkStatus::InProgress => Self::InProgress,
TicketWorkStatus::Queued => Self::Queued,
}
}
}
/// Origin of a queued ticket (human operator or a delegating agent).
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum TicketWorkSourceDto {
/// The human operator.
Human,
/// Another agent delegating via `idea_ask_agent`.
#[serde(rename_all = "camelCase")]
Agent {
/// The delegating agent id.
agent_id: String,
},
}
impl From<TicketWorkSource> for TicketWorkSourceDto {
fn from(source: TicketWorkSource) -> Self {
match source {
TicketWorkSource::Human => Self::Human,
TicketWorkSource::Agent { agent_id } => Self::Agent {
agent_id: agent_id.to_string(),
},
}
}
}
/// One queued/in-progress delegation ticket for an agent.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTicketStateDto {
/// Stable id of the queued ticket.
pub ticket_id: String,
/// Conversation thread this task enters.
pub conversation_id: String,
/// FIFO position at snapshot time (`0` = head).
pub position: u32,
/// Derived status (in-progress vs queued).
pub status: TicketWorkStatusDto,
/// Origin of the ticket.
pub source: TicketWorkSourceDto,
/// Display label of the requester.
pub requester_label: String,
/// Bounded excerpt of the task.
pub task_preview: String,
/// Character length of the original (un-truncated) task.
pub task_len: usize,
}
impl From<AgentTicketState> for AgentTicketStateDto {
fn from(ticket: AgentTicketState) -> Self {
Self {
ticket_id: ticket.ticket_id.to_string(),
conversation_id: ticket.conversation_id.to_string(),
position: ticket.position,
status: ticket.status.into(),
source: ticket.source.into(),
requester_label: ticket.requester_label,
task_preview: ticket.task_preview,
task_len: ticket.task_len,
}
}
}
/// One manifest agent's current live/busy state. /// One manifest agent's current live/busy state.
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -1517,6 +1599,8 @@ pub struct AgentWorkStateDto {
pub live: Option<LiveWorkSessionDto>, pub live: Option<LiveWorkSessionDto>,
/// Current mediated-input busy state. /// Current mediated-input busy state.
pub busy: AgentBusyState, pub busy: AgentBusyState,
/// Pending/in-progress delegation tickets, in FIFO order.
pub tickets: Vec<AgentTicketStateDto>,
} }
/// Project-level read model for conversation/delegation UX. /// Project-level read model for conversation/delegation UX.
@ -1543,6 +1627,11 @@ impl From<ProjectWorkState> for ProjectWorkStateDto {
kind: live.kind.into(), kind: live.kind.into(),
}), }),
busy: agent.busy, busy: agent.busy,
tickets: agent
.tickets
.into_iter()
.map(AgentTicketStateDto::from)
.collect(),
}) })
.collect(), .collect(),
} }

View File

@ -993,6 +993,10 @@ impl AppState {
// partagé pour `resolve`/`resolve_ticket`/`cancel_head` côté orchestrateur. // partagé pour `resolve`/`resolve_ticket`/`cancel_head` côté orchestrateur.
let inmemory_mailbox = Arc::new(InMemoryMailbox::new()); let inmemory_mailbox = Arc::new(InMemoryMailbox::new());
let mailbox = Arc::clone(&inmemory_mailbox) as Arc<dyn domain::mailbox::AgentMailbox>; let mailbox = Arc::clone(&inmemory_mailbox) as Arc<dyn domain::mailbox::AgentMailbox>;
// Same concrete mailbox, second (read-only) port view for the work-state
// read model: lists pending tickets without touching the mutating surface.
let queue_snapshot =
Arc::clone(&inmemory_mailbox) as Arc<dyn domain::mailbox::AgentQueueSnapshot>;
let mediated_inbox = Arc::new( let mediated_inbox = Arc::new(
MediatedInbox::with_pty( MediatedInbox::with_pty(
Arc::clone(&inmemory_mailbox), Arc::clone(&inmemory_mailbox),
@ -1031,6 +1035,7 @@ impl AppState {
Arc::clone(&contexts_port), Arc::clone(&contexts_port),
Arc::clone(&live_sessions), Arc::clone(&live_sessions),
Arc::clone(&input_mediator), Arc::clone(&input_mediator),
queue_snapshot,
)); ));
// --- Limites de session des agents (ARCHITECTURE §21, LS7) --- // --- Limites de session des agents (ARCHITECTURE §21, LS7) ---

View File

@ -9,8 +9,9 @@ use app_tauri_lib::dto::{
}; };
use application::AppError; use application::AppError;
use application::{ use application::{
AgentWorkState, CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, AgentTicketState, AgentWorkState, CreateAgentOutput, InspectConversationOutput,
ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState, LaunchAgentOutput, ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState,
TicketWorkSource, TicketWorkStatus,
}; };
use domain::ids::{AgentId, NodeId, ProfileId, SessionId}; use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
use domain::ports::ConversationDetails; use domain::ports::ConversationDetails;
@ -219,6 +220,7 @@ fn project_work_state_dto_serialises_live_and_busy_camelcase() {
ticket, ticket,
since_ms: 1_234, since_ms: 1_234,
}, },
tickets: vec![],
}], }],
}); });
@ -231,11 +233,77 @@ fn project_work_state_dto_serialises_live_and_busy_camelcase() {
assert_eq!(v["agents"][0]["busy"]["state"], "busy"); assert_eq!(v["agents"][0]["busy"]["state"], "busy");
assert_eq!(v["agents"][0]["busy"]["ticket"], ticket.to_string()); assert_eq!(v["agents"][0]["busy"]["ticket"], ticket.to_string());
assert_eq!(v["agents"][0]["busy"]["sinceMs"], 1_234); assert_eq!(v["agents"][0]["busy"]["sinceMs"], 1_234);
assert_eq!(v["agents"][0]["tickets"], json!([]));
assert!(v["agents"][0].get("agent_id").is_none()); assert!(v["agents"][0].get("agent_id").is_none());
assert!(v["agents"][0]["live"].get("session_id").is_none()); assert!(v["agents"][0]["live"].get("session_id").is_none());
assert!(v["agents"][0]["busy"].get("since_ms").is_none()); assert!(v["agents"][0]["busy"].get("since_ms").is_none());
} }
#[test]
fn project_work_state_dto_serialises_tickets_camelcase() {
let agent = AgentId::from_uuid(Uuid::from_u128(11));
let profile = ProfileId::from_uuid(Uuid::from_u128(12));
let requester = AgentId::from_uuid(Uuid::from_u128(20));
let head_ticket = domain::TicketId::from_uuid(Uuid::from_u128(30));
let next_ticket = domain::TicketId::from_uuid(Uuid::from_u128(31));
let conversation = domain::ConversationId::from_uuid(Uuid::from_u128(40));
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
agents: vec![AgentWorkState {
agent_id: agent,
name: "Worker".to_owned(),
profile_id: profile,
live: None,
busy: domain::AgentBusyState::Idle,
tickets: vec![
AgentTicketState {
ticket_id: head_ticket,
conversation_id: conversation,
position: 0,
status: TicketWorkStatus::InProgress,
source: TicketWorkSource::Agent {
agent_id: requester,
},
requester_label: "Main".to_owned(),
task_preview: "Analyser le module".to_owned(),
task_len: 842,
},
AgentTicketState {
ticket_id: next_ticket,
conversation_id: conversation,
position: 1,
status: TicketWorkStatus::Queued,
source: TicketWorkSource::Human,
requester_label: "User".to_owned(),
task_preview: "Vérifier".to_owned(),
task_len: 8,
},
],
}],
});
let v = serde_json::to_value(&dto).unwrap();
let head = &v["agents"][0]["tickets"][0];
assert_eq!(head["ticketId"], head_ticket.to_string());
assert_eq!(head["conversationId"], conversation.to_string());
assert_eq!(head["position"], 0);
assert_eq!(head["status"], "inProgress");
assert_eq!(head["source"]["kind"], "agent");
assert_eq!(head["source"]["agentId"], requester.to_string());
assert_eq!(head["requesterLabel"], "Main");
assert_eq!(head["taskPreview"], "Analyser le module");
assert_eq!(head["taskLen"], 842);
// snake_case must not leak.
assert!(head.get("ticket_id").is_none());
assert!(head.get("task_preview").is_none());
assert!(head["source"].get("agent_id").is_none());
let next = &v["agents"][0]["tickets"][1];
assert_eq!(next["status"], "queued");
assert_eq!(next["source"]["kind"], "human");
assert!(next["source"].get("agentId").is_none());
assert_eq!(next["requesterLabel"], "User");
}
#[test] #[test]
fn launch_agent_request_carries_conversation_id_for_resume() { fn launch_agent_request_carries_conversation_id_for_resume() {
let raw = json!({ let raw = json!({

View File

@ -119,6 +119,6 @@ pub use terminal::{
}; };
pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput}; pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
pub use workstate::{ pub use workstate::{
AgentWorkState, GetProjectWorkState, GetProjectWorkStateInput, LiveWorkSession, AgentTicketState, AgentWorkState, GetProjectWorkState, GetProjectWorkStateInput,
ProjectWorkState, LiveWorkSession, ProjectWorkState, TicketWorkSource, TicketWorkStatus,
}; };

View File

@ -8,13 +8,22 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use domain::input::AgentBusyState; use domain::input::{AgentBusyState, InputSource};
use domain::ports::AgentContextStore; use domain::ports::AgentContextStore;
use domain::{AgentId, InputMediator, NodeId, ProfileId, Project, SessionId}; use domain::{
AgentId, AgentQueueSnapshot, ConversationId, InputMediator, NodeId, ProfileId, Project,
QueuedTicketSnapshot, SessionId, TicketId,
};
use crate::error::AppError; use crate::error::AppError;
use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions}; use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions};
/// Maximum length (in characters) of a derived [`AgentTicketState::task_preview`].
///
/// The full task can be a very long prompt; the panel only needs a scannable
/// excerpt, with [`AgentTicketState::task_len`] signalling there is more.
const TASK_PREVIEW_MAX_CHARS: usize = 160;
/// Input for [`GetProjectWorkState::execute`]. /// Input for [`GetProjectWorkState::execute`].
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetProjectWorkStateInput { pub struct GetProjectWorkStateInput {
@ -42,6 +51,64 @@ pub struct AgentWorkState {
pub live: Option<LiveWorkSession>, pub live: Option<LiveWorkSession>,
/// Current FIFO/busy state. /// Current FIFO/busy state.
pub busy: AgentBusyState, pub busy: AgentBusyState,
/// Pending/in-progress delegation tickets, in FIFO order.
pub tickets: Vec<AgentTicketState>,
}
/// One queued delegation ticket, projected for the work-state read model.
///
/// A read-only, presentation-oriented view of a [`QueuedTicketSnapshot`]: the
/// status is **derived** by crossing the FIFO snapshot with the agent's busy state
/// (never stored), and `task_preview` is a bounded excerpt of the full task (the
/// full text is never sent to the panel; `task_len` signals there is more).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentTicketState {
/// Stable id of the queued ticket.
pub ticket_id: TicketId,
/// Conversation thread this task enters.
pub conversation_id: ConversationId,
/// FIFO position at snapshot time (`0` = head).
pub position: u32,
/// Derived status (in-progress = the agent's current busy ticket).
pub status: TicketWorkStatus,
/// Origin of the ticket (human operator or a delegating agent).
pub source: TicketWorkSource,
/// Display label of the requester.
pub requester_label: String,
/// Bounded, whitespace-normalised excerpt of the task.
pub task_preview: String,
/// Character length of the **original** (un-truncated) task.
pub task_len: usize,
}
/// Derived processing status of a queued ticket.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TicketWorkStatus {
/// The agent's current busy turn is running this ticket.
InProgress,
/// Waiting behind the head / the agent is idle.
Queued,
}
/// Origin of a queued ticket, mirroring [`InputSource`] for the read model.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TicketWorkSource {
/// The human operator.
Human,
/// Another agent delegating via `idea_ask_agent`.
Agent {
/// The delegating agent.
agent_id: AgentId,
},
}
impl From<InputSource> for TicketWorkSource {
fn from(source: InputSource) -> Self {
match source {
InputSource::Human => Self::Human,
InputSource::Agent { agent_id } => Self::Agent { agent_id },
}
}
} }
/// Live session coordinates exposed by the work-state read model. /// Live session coordinates exposed by the work-state read model.
@ -60,6 +127,7 @@ pub struct GetProjectWorkState {
contexts: Arc<dyn AgentContextStore>, contexts: Arc<dyn AgentContextStore>,
live: Arc<LiveSessions>, live: Arc<LiveSessions>,
input: Arc<dyn InputMediator>, input: Arc<dyn InputMediator>,
queue: Arc<dyn AgentQueueSnapshot>,
} }
impl GetProjectWorkState { impl GetProjectWorkState {
@ -69,11 +137,13 @@ impl GetProjectWorkState {
contexts: Arc<dyn AgentContextStore>, contexts: Arc<dyn AgentContextStore>,
live: Arc<LiveSessions>, live: Arc<LiveSessions>,
input: Arc<dyn InputMediator>, input: Arc<dyn InputMediator>,
queue: Arc<dyn AgentQueueSnapshot>,
) -> Self { ) -> Self {
Self { Self {
contexts, contexts,
live, live,
input, input,
queue,
} }
} }
@ -102,12 +172,23 @@ impl GetProjectWorkState {
session_id: snapshot.session_id, session_id: snapshot.session_id,
kind: snapshot.kind, kind: snapshot.kind,
}); });
// Tickets are crossed with the busy state: only an agent absent from
// the manifest is dropped (this loop only visits manifest entries), so
// the manifest boundary is naturally preserved.
let busy_ticket = self.input.busy_state(agent.id).ticket();
let tickets = self
.queue
.queue_for(agent.id)
.into_iter()
.map(|snapshot| ticket_state(snapshot, busy_ticket))
.collect();
Ok(AgentWorkState { Ok(AgentWorkState {
agent_id: agent.id, agent_id: agent.id,
name: agent.name, name: agent.name,
profile_id: agent.profile_id, profile_id: agent.profile_id,
live, live,
busy: self.input.busy_state(agent.id), busy: self.input.busy_state(agent.id),
tickets,
}) })
}) })
.collect::<Result<Vec<_>, AppError>>()?; .collect::<Result<Vec<_>, AppError>>()?;
@ -115,6 +196,40 @@ impl GetProjectWorkState {
} }
} }
/// Projects one FIFO snapshot into a read-model ticket, deriving status from the
/// agent's current busy ticket (the one running its turn = `InProgress`).
fn ticket_state(snapshot: QueuedTicketSnapshot, busy_ticket: Option<TicketId>) -> AgentTicketState {
let status = if busy_ticket == Some(snapshot.id) {
TicketWorkStatus::InProgress
} else {
TicketWorkStatus::Queued
};
AgentTicketState {
ticket_id: snapshot.id,
conversation_id: snapshot.conversation,
position: snapshot.position,
status,
source: snapshot.source.into(),
requester_label: snapshot.requester,
task_preview: task_preview(&snapshot.task),
task_len: snapshot.task.chars().count(),
}
}
/// Builds a bounded, whitespace-normalised excerpt of a task for the UI panel.
///
/// Trims, collapses any run of whitespace to a single space, then truncates to
/// [`TASK_PREVIEW_MAX_CHARS`] characters (never mid-codepoint). The original length
/// is reported separately as [`AgentTicketState::task_len`].
fn task_preview(task: &str) -> String {
let normalised = task.split_whitespace().collect::<Vec<_>>().join(" ");
if normalised.chars().count() <= TASK_PREVIEW_MAX_CHARS {
normalised
} else {
normalised.chars().take(TASK_PREVIEW_MAX_CHARS).collect()
}
}
fn live_by_agent(snapshots: Vec<LiveSessionSnapshot>) -> HashMap<AgentId, LiveSessionSnapshot> { fn live_by_agent(snapshots: Vec<LiveSessionSnapshot>) -> HashMap<AgentId, LiveSessionSnapshot> {
let mut out = HashMap::new(); let mut out = HashMap::new();
for snapshot in snapshots { for snapshot in snapshots {

View File

@ -9,16 +9,18 @@ use async_trait::async_trait;
use application::{ use application::{
GetProjectWorkState, GetProjectWorkStateInput, LiveSessionKind, LiveSessions, GetProjectWorkState, GetProjectWorkStateInput, LiveSessionKind, LiveSessions,
StructuredSessions, TerminalSessions, StructuredSessions, TerminalSessions, TicketWorkSource, TicketWorkStatus,
};
use domain::mailbox::{
AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket,
}; };
use domain::mailbox::{MailboxError, PendingReply, Ticket};
use domain::ports::{ use domain::ports::{
AgentContextStore, AgentSession, AgentSessionError, PtyHandle, ReplyStream, StoreError, AgentContextStore, AgentSession, AgentSessionError, PtyHandle, ReplyStream, StoreError,
}; };
use domain::{ use domain::{
Agent, AgentBusyState, AgentId, AgentManifest, AgentOrigin, InputMediator, ManifestEntry, Agent, AgentBusyState, AgentId, AgentManifest, AgentOrigin, ConversationId, InputMediator,
MarkdownDoc, NodeId, ProfileId, Project, ProjectId, ProjectPath, PtySize, RemoteRef, SessionId, InputSource, ManifestEntry, MarkdownDoc, NodeId, ProfileId, Project, ProjectId, ProjectPath,
SessionKind, TerminalSession, PtySize, RemoteRef, SessionId, SessionKind, TerminalSession, TicketId,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -138,6 +140,45 @@ impl InputMediator for FakeInput {
} }
} }
#[derive(Default)]
struct FakeQueue {
queues: Mutex<HashMap<AgentId, Vec<QueuedTicketSnapshot>>>,
}
impl FakeQueue {
fn set(&self, agent: AgentId, tickets: Vec<QueuedTicketSnapshot>) {
self.queues.lock().unwrap().insert(agent, tickets);
}
}
impl AgentQueueSnapshot for FakeQueue {
fn queue_for(&self, agent: AgentId) -> Vec<QueuedTicketSnapshot> {
self.queues
.lock()
.unwrap()
.get(&agent)
.cloned()
.unwrap_or_default()
}
}
fn snapshot(
id: u128,
position: u32,
source: InputSource,
requester: &str,
task: &str,
) -> QueuedTicketSnapshot {
QueuedTicketSnapshot {
id: TicketId::from_uuid(Uuid::from_u128(id)),
source,
conversation: ConversationId::from_uuid(Uuid::from_u128(id + 1000)),
requester: requester.to_owned(),
task: task.to_owned(),
position,
}
}
struct FakeSession { struct FakeSession {
id: SessionId, id: SessionId,
} }
@ -188,6 +229,7 @@ struct Fixture {
pty: Arc<TerminalSessions>, pty: Arc<TerminalSessions>,
structured: Arc<StructuredSessions>, structured: Arc<StructuredSessions>,
input: Arc<FakeInput>, input: Arc<FakeInput>,
queue: Arc<FakeQueue>,
project: Project, project: Project,
} }
@ -197,18 +239,22 @@ fn fixture(agents: &[Agent]) -> Fixture {
let live = Arc::new(LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured))); let live = Arc::new(LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured)));
let input = Arc::new(FakeInput::default()); let input = Arc::new(FakeInput::default());
let input_port = Arc::clone(&input) as Arc<dyn InputMediator>; let input_port = Arc::clone(&input) as Arc<dyn InputMediator>;
let queue = Arc::new(FakeQueue::default());
let queue_port = Arc::clone(&queue) as Arc<dyn AgentQueueSnapshot>;
let usecase = GetProjectWorkState::new( let usecase = GetProjectWorkState::new(
Arc::new(FakeContexts { Arc::new(FakeContexts {
manifest: manifest(agents), manifest: manifest(agents),
}), }),
live, live,
input_port, input_port,
queue_port,
); );
Fixture { Fixture {
usecase, usecase,
pty, pty,
structured, structured,
input, input,
queue,
project: project(), project: project(),
} }
} }
@ -305,3 +351,169 @@ async fn workstate_ignores_live_agents_absent_from_manifest() {
assert_eq!(out.agents[0].agent_id, a.id); assert_eq!(out.agents[0].agent_id, a.id);
assert_eq!(out.agents[0].live, None); assert_eq!(out.agents[0].live, None);
} }
#[tokio::test]
async fn workstate_agent_without_queue_has_no_tickets() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
assert!(out.agents[0].tickets.is_empty());
}
#[tokio::test]
async fn workstate_lists_two_tickets_in_fifo_order() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![
snapshot(1, 0, InputSource::agent(from), "Main", "first"),
snapshot(2, 1, InputSource::agent(from), "Main", "second"),
],
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let tickets = &out.agents[0].tickets;
assert_eq!(tickets.len(), 2);
assert_eq!(tickets[0].ticket_id, ticket_id(1));
assert_eq!(tickets[0].position, 0);
assert_eq!(tickets[1].ticket_id, ticket_id(2));
assert_eq!(tickets[1].position, 1);
}
#[tokio::test]
async fn workstate_marks_busy_head_in_progress_and_rest_queued() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![
snapshot(1, 0, InputSource::agent(from), "Main", "first"),
snapshot(2, 1, InputSource::agent(from), "Main", "second"),
],
);
f.input.set_busy(
a.id,
AgentBusyState::Busy {
ticket: ticket_id(1),
since_ms: 5,
},
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let tickets = &out.agents[0].tickets;
assert_eq!(tickets[0].status, TicketWorkStatus::InProgress);
assert_eq!(tickets[1].status, TicketWorkStatus::Queued);
}
#[tokio::test]
async fn workstate_marks_all_queued_when_agent_idle() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "first")],
);
// Agent left Idle (default): even the head is only queued, not in-progress.
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
assert_eq!(out.agents[0].tickets[0].status, TicketWorkStatus::Queued);
}
#[tokio::test]
async fn workstate_ignores_queue_for_agent_absent_from_manifest() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
// A queue exists for an agent that is not in the manifest: it must not surface.
f.queue.set(
aid(999),
vec![snapshot(1, 0, InputSource::Human, "User", "ghost")],
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
assert_eq!(out.agents.len(), 1);
assert!(out.agents[0].tickets.is_empty());
}
#[tokio::test]
async fn workstate_truncates_task_preview_and_keeps_original_len() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
// 400 chars with runs of whitespace to normalise; well over the 160 cap.
let long_task = format!("start{}end", " x ".repeat(80));
let original_len = long_task.chars().count();
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::Human, "User", &long_task)],
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let ticket = &out.agents[0].tickets[0];
assert_eq!(ticket.task_preview.chars().count(), 160);
assert!(!ticket.task_preview.contains(" "), "whitespace normalised");
assert_eq!(ticket.task_len, original_len);
assert!(ticket.task_len > 160);
}
#[tokio::test]
async fn workstate_maps_human_and_agent_ticket_sources() {
let a = agent(10, "alpha");
let f = fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![
snapshot(1, 0, InputSource::Human, "User", "from human"),
snapshot(2, 1, InputSource::agent(from), "Main", "from agent"),
],
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let tickets = &out.agents[0].tickets;
assert_eq!(tickets[0].source, TicketWorkSource::Human);
assert_eq!(tickets[0].requester_label, "User");
assert_eq!(
tickets[1].source,
TicketWorkSource::Agent { agent_id: from }
);
assert_eq!(tickets[1].requester_label, "Main");
}

View File

@ -82,7 +82,10 @@ pub use profile::{
McpServerWiring, RateLimitPattern, SessionStrategy, McpServerWiring, RateLimitPattern, SessionStrategy,
}; };
pub use mailbox::{AgentMailbox, MailboxError, PendingReply, Ticket, TicketId}; pub use mailbox::{
AgentMailbox, AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket,
TicketId,
};
pub use conversation::{ pub use conversation::{
Conversation, ConversationError, ConversationId, ConversationParty, ConversationRegistry, Conversation, ConversationError, ConversationId, ConversationParty, ConversationRegistry,

View File

@ -33,6 +33,47 @@ use crate::conversation::ConversationId;
use crate::ids::AgentId; use crate::ids::AgentId;
use crate::input::InputSource; use crate::input::InputSource;
/// A read-only, cloned view of one queued [`Ticket`] in a target agent's FIFO.
///
/// Carries **only the data** of a ticket (never the one-shot reply sender, which
/// stays inside the adapter), plus its `position` in the FIFO at snapshot time
/// (`0` = head). Pure value object used by the [`AgentQueueSnapshot`] read port so
/// the work-state read model can list pending delegations without touching the
/// mutating [`AgentMailbox`] surface (ISP).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueuedTicketSnapshot {
/// Stable id of the queued ticket.
pub id: TicketId,
/// The origin of the input (Human or a delegating Agent).
pub source: InputSource,
/// The conversation thread this task enters.
pub conversation: ConversationId,
/// Display name (or id) of the requester.
pub requester: String,
/// The full task/message text (preview/truncation is the caller's concern).
pub task: String,
/// Position in the FIFO at snapshot time (`0` = head, recomputed each call).
pub position: u32,
}
/// Read-only inspection port over the per-agent delegation FIFO.
///
/// Segregated from the mutating [`AgentMailbox`] (Interface Segregation): the
/// work-state read model depends on this port to *observe* the queue, never on
/// `enqueue`/`resolve`/`cancel`. Implementations return **cloned** snapshots in
/// FIFO order with recomputed positions; observing the queue must never mutate it.
///
/// Object-safe (`&self`) so the application layer holds it as
/// `Arc<dyn AgentQueueSnapshot>`; one concrete `InMemoryMailbox` can be shared as
/// both an [`AgentMailbox`] (mutation) and an [`AgentQueueSnapshot`] (read) view.
pub trait AgentQueueSnapshot: Send + Sync {
/// Returns a cloned, FIFO-ordered snapshot of `agent`'s queued tickets.
///
/// An agent with no queue yields an empty `Vec`. Positions are recomputed from
/// the current order (`0` = head). Pure read: the queue is left untouched.
fn queue_for(&self, agent: AgentId) -> Vec<QueuedTicketSnapshot>;
}
/// Identifies one queued [`Ticket`] within a target agent's mailbox. /// Identifies one queued [`Ticket`] within a target agent's mailbox.
/// ///
/// Newtype around [`uuid::Uuid`]; minted by the adapter on `enqueue`. It is **never /// Newtype around [`uuid::Uuid`]; minted by the adapter on `enqueue`. It is **never
@ -310,4 +351,22 @@ mod tests {
let a = AgentId::from_uuid(uuid::Uuid::from_u128(1)); let a = AgentId::from_uuid(uuid::Uuid::from_u128(1));
assert_ne!(MailboxError::NoPendingRequest(a), MailboxError::Cancelled); assert_ne!(MailboxError::NoPendingRequest(a), MailboxError::Cancelled);
} }
#[test]
fn queued_ticket_snapshot_carries_ticket_data_and_position() {
let conv = ConversationId::from_uuid(uuid::Uuid::from_u128(3));
let from = AgentId::from_uuid(uuid::Uuid::from_u128(4));
let snap = QueuedTicketSnapshot {
id: TicketId::from_uuid(uuid::Uuid::from_u128(8)),
source: InputSource::agent(from),
conversation: conv,
requester: "Main".to_owned(),
task: "delegate".to_owned(),
position: 2,
};
assert_eq!(snap.source.as_agent(), Some(from));
assert_eq!(snap.conversation, conv);
assert_eq!(snap.position, 2);
assert_eq!(snap.task, "delegate");
}
} }

View File

@ -25,7 +25,10 @@ use std::sync::Mutex;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use domain::ids::AgentId; use domain::ids::AgentId;
use domain::mailbox::{AgentMailbox, MailboxError, PendingReply, Ticket, TicketId}; use domain::mailbox::{
AgentMailbox, AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket,
TicketId,
};
/// One queued request plus the sender that resolves its awaiting [`PendingReply`]. /// One queued request plus the sender that resolves its awaiting [`PendingReply`].
struct Slot { struct Slot {
@ -160,6 +163,35 @@ impl AgentMailbox for InMemoryMailbox {
} }
} }
impl AgentQueueSnapshot for InMemoryMailbox {
fn queue_for(&self, agent: AgentId) -> Vec<QueuedTicketSnapshot> {
// Pure read: clone each ticket's *data* (never the `oneshot::Sender`) under
// the lock, recomputing the FIFO position from the current order (0 = head).
// The queue is observed, not mutated.
let queues = self
.queues
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
queues
.get(&agent)
.map(|queue| {
queue
.iter()
.enumerate()
.map(|(idx, slot)| QueuedTicketSnapshot {
id: slot.ticket.id,
source: slot.ticket.source,
conversation: slot.ticket.conversation,
requester: slot.ticket.requester.clone(),
task: slot.ticket.task.clone(),
position: u32::try_from(idx).unwrap_or(u32::MAX),
})
.collect()
})
.unwrap_or_default()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -270,4 +302,98 @@ mod tests {
Some(TicketId::from_uuid(uuid::Uuid::from_u128(10))) Some(TicketId::from_uuid(uuid::Uuid::from_u128(10)))
); );
} }
fn tid(n: u128) -> TicketId {
TicketId::from_uuid(uuid::Uuid::from_u128(n))
}
#[test]
fn snapshot_of_empty_queue_is_empty() {
let mb = InMemoryMailbox::new();
assert!(mb.queue_for(agent(1)).is_empty());
}
#[test]
fn snapshot_preserves_fifo_order_and_positions() {
let mb = InMemoryMailbox::new();
let a = agent(1);
let _p1 = mb.enqueue(a, ticket(10, "first"));
let _p2 = mb.enqueue(a, ticket(11, "second"));
let snap = mb.queue_for(a);
assert_eq!(snap.len(), 2);
assert_eq!(snap[0].id, tid(10));
assert_eq!(snap[0].position, 0);
assert_eq!(snap[0].task, "first");
assert_eq!(snap[1].id, tid(11));
assert_eq!(snap[1].position, 1);
assert_eq!(snap[1].task, "second");
}
#[test]
fn snapshot_carries_ticket_metadata() {
use domain::conversation::ConversationId;
use domain::input::InputSource;
let mb = InMemoryMailbox::new();
let a = agent(1);
let from = agent(2);
let conv = ConversationId::from_uuid(uuid::Uuid::from_u128(42));
let _p = mb.enqueue(
a,
Ticket::from_agent(tid(10), from, conv, "Main", "delegate task"),
);
let snap = mb.queue_for(a);
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].source, InputSource::agent(from));
assert_eq!(snap[0].conversation, conv);
assert_eq!(snap[0].requester, "Main");
assert_eq!(snap[0].task, "delegate task");
}
#[test]
fn snapshot_is_read_only() {
let mb = InMemoryMailbox::new();
let a = agent(1);
let _p1 = mb.enqueue(a, ticket(10, "first"));
let _p2 = mb.enqueue(a, ticket(11, "second"));
let _ = mb.queue_for(a);
let _ = mb.queue_for(a);
// Observing the queue did not mutate it.
assert_eq!(mb.pending(&a), 2);
assert_eq!(mb.head_ticket(&a), Some(tid(10)));
}
#[test]
fn snapshot_updates_after_resolve_ticket() {
let mb = InMemoryMailbox::new();
let a = agent(1);
let _p1 = mb.enqueue(a, ticket(10, "first"));
let _p2 = mb.enqueue(a, ticket(11, "second"));
mb.resolve_ticket(a, tid(10), "done".to_owned()).unwrap();
let snap = mb.queue_for(a);
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].id, tid(11));
assert_eq!(snap[0].position, 0, "remaining ticket becomes head");
}
#[test]
fn snapshot_updates_after_cancel_head() {
let mb = InMemoryMailbox::new();
let a = agent(1);
let _p1 = mb.enqueue(a, ticket(10, "head"));
let _p2 = mb.enqueue(a, ticket(11, "next"));
mb.cancel_head(a, tid(10));
let snap = mb.queue_for(a);
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].id, tid(11));
assert_eq!(snap[0].position, 0, "next ticket recomputed to position 0");
}
} }

View File

@ -1654,8 +1654,11 @@ export class MockWorkStateGateway implements WorkStateGateway {
private states = new Map<string, ProjectWorkState>(); private states = new Map<string, ProjectWorkState>();
/** Seeds the read-model returned for a project (deterministic tests/dev). */ /** Seeds the read-model returned for a project (deterministic tests/dev). */
_setProjectWorkState(projectId: string, state: ProjectWorkState): void { _setProjectWorkState(
this.states.set(projectId, structuredClone(state)); projectId: string,
state: ProjectWorkState | LegacyProjectWorkState,
): void {
this.states.set(projectId, normalizeProjectWorkState(state));
} }
async getProjectWorkState(projectId: string): Promise<ProjectWorkState> { async getProjectWorkState(projectId: string): Promise<ProjectWorkState> {
@ -1663,6 +1666,23 @@ export class MockWorkStateGateway implements WorkStateGateway {
} }
} }
type LegacyProjectWorkState = {
agents: Array<Omit<ProjectWorkState["agents"][number], "tickets"> & {
tickets?: ProjectWorkState["agents"][number]["tickets"];
}>;
};
function normalizeProjectWorkState(
state: ProjectWorkState | LegacyProjectWorkState,
): ProjectWorkState {
return {
agents: state.agents.map((agent) => ({
...structuredClone(agent),
tickets: structuredClone(agent.tickets ?? []),
})),
};
}
function mostRestrictive( function mostRestrictive(
project?: PermissionSet["fallback"], project?: PermissionSet["fallback"],
agent?: PermissionSet["fallback"], agent?: PermissionSet["fallback"],

View File

@ -149,6 +149,26 @@ export type WorkBusyState =
| { state: "idle" } | { state: "idle" }
| { state: "busy"; ticket: string; sinceMs: number }; | { state: "busy"; ticket: string; sinceMs: number };
/** FIFO status for an input/ticket currently tracked by the backend queue. */
export type TicketWorkStatus = "inProgress" | "queued";
/** Who produced an input/ticket in the work-state read-model. */
export type TicketWorkSource =
| { kind: "human" }
| { kind: "agent"; agentId: string };
/** One queued or in-progress input/ticket for an agent. */
export interface AgentTicketState {
ticketId: string;
conversationId: string;
position: number;
status: TicketWorkStatus;
source: TicketWorkSource;
requesterLabel: string;
taskPreview: string;
taskLen: number;
}
/** One agent row in the project work-state read-model. */ /** One agent row in the project work-state read-model. */
export interface AgentWorkState { export interface AgentWorkState {
agentId: string; agentId: string;
@ -156,6 +176,7 @@ export interface AgentWorkState {
profileId: string; profileId: string;
live?: LiveWorkSession; live?: LiveWorkSession;
busy: WorkBusyState; busy: WorkBusyState;
tickets: AgentTicketState[];
} }
/** Minimal read-only live-state surface for a project. */ /** Minimal read-only live-state surface for a project. */

View File

@ -1,9 +1,9 @@
/** /**
* Read-only project work-state panel: one row per agent, showing live/offline * Read-only project work-state panel: one row per agent, showing live/offline
* and idle/busy state from the backend read-model. * and idle/busy state from the backend read-model, plus the current input queue.
*/ */
import type { AgentWorkState } from "@/domain"; import type { AgentTicketState, AgentWorkState } from "@/domain";
import { Button, Panel, Spinner, cn } from "@/shared"; import { Button, Panel, Spinner, cn } from "@/shared";
import { useProjectWorkState } from "./useProjectWorkState"; import { useProjectWorkState } from "./useProjectWorkState";
@ -15,48 +15,112 @@ function shortTicket(ticket: string): string {
return ticket.length <= 8 ? ticket : ticket.slice(0, 8); return ticket.length <= 8 ? ticket : ticket.slice(0, 8);
} }
function formatStatus(status: AgentTicketState["status"]): string {
return status === "inProgress" ? "In progress" : "Queued";
}
function requesterLabel(ticket: AgentTicketState): string {
if (ticket.source.kind === "human") return "Human";
const id = shortTicket(ticket.source.agentId);
return ticket.requesterLabel.trim()
? `${ticket.requesterLabel} (${id})`
: `Agent ${id}`;
}
function TicketRow({ ticket }: { ticket: AgentTicketState }) {
const truncated = ticket.taskLen > ticket.taskPreview.length;
return (
<li className="flex min-w-0 items-start gap-2 text-xs text-muted">
<span
className={cn(
"mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
ticket.status === "inProgress"
? "bg-warning/15 text-warning"
: "bg-raised text-muted",
)}
>
#{ticket.position + 1} {formatStatus(ticket.status)}
</span>
<span className="min-w-0 flex-1">
<span className="font-medium text-content">{requesterLabel(ticket)}</span>
<span className="text-muted"> · </span>
<span className="break-words">{ticket.taskPreview}</span>
{truncated && (
<span
aria-label={`${ticket.taskLen - ticket.taskPreview.length} more characters`}
className="text-muted"
>
{" "}
+{ticket.taskLen - ticket.taskPreview.length}
</span>
)}
</span>
<code
aria-label={`ticket ${shortTicket(ticket.ticketId)}`}
title={ticket.ticketId}
className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
>
{shortTicket(ticket.ticketId)}
</code>
</li>
);
}
function AgentRow({ agent }: { agent: AgentWorkState }) { function AgentRow({ agent }: { agent: AgentWorkState }) {
const live = agent.live !== undefined; const live = agent.live !== undefined;
const busy = agent.busy.state === "busy"; const busy = agent.busy.state === "busy";
const tickets = [...agent.tickets].sort((a, b) => a.position - b.position);
return ( return (
<li className="flex items-center justify-between gap-3 py-2 first:pt-0 last:pb-0"> <li className="py-2 first:pt-0 last:pb-0">
<span className="flex min-w-0 flex-col gap-0.5"> <div className="flex items-center justify-between gap-3">
<span className="truncate text-sm font-medium text-content"> <span className="flex min-w-0 flex-col gap-0.5">
{agent.name} <span className="truncate text-sm font-medium text-content">
{agent.name}
</span>
<code className="truncate text-xs text-muted">{agent.profileId}</code>
</span> </span>
<code className="truncate text-xs text-muted">{agent.profileId}</code> <span className="flex shrink-0 items-center gap-1.5">
</span> <span
<span className="flex shrink-0 items-center gap-1.5"> className={cn(
<span "rounded-full px-2 py-0.5 text-xs font-medium",
className={cn( live
"rounded-full px-2 py-0.5 text-xs font-medium", ? "bg-success/15 text-success"
live : "bg-raised text-muted",
? "bg-success/15 text-success" )}
: "bg-raised text-muted",
)}
>
{live ? "Live" : "Offline"}
</span>
<span
className={cn(
"rounded-full px-2 py-0.5 text-xs font-medium",
busy
? "bg-warning/15 text-warning"
: "bg-raised text-muted",
)}
>
{busy ? "Busy" : "Idle"}
</span>
{agent.busy.state === "busy" && (
<code
aria-label={`busy ticket ${shortTicket(agent.busy.ticket)}`}
title={agent.busy.ticket}
className="rounded bg-raised px-1.5 py-0.5 text-xs text-muted"
> >
{shortTicket(agent.busy.ticket)} {live ? "Live" : "Offline"}
</code> </span>
)} <span
</span> className={cn(
"rounded-full px-2 py-0.5 text-xs font-medium",
busy
? "bg-warning/15 text-warning"
: "bg-raised text-muted",
)}
>
{busy ? "Busy" : "Idle"}
</span>
{agent.busy.state === "busy" && (
<code
aria-label={`busy ticket ${shortTicket(agent.busy.ticket)}`}
title={agent.busy.ticket}
className="rounded bg-raised px-1.5 py-0.5 text-xs text-muted"
>
{shortTicket(agent.busy.ticket)}
</code>
)}
</span>
</div>
{tickets.length > 0 && (
<ul
aria-label={`${agent.name} tickets`}
className="mt-2 flex flex-col gap-1"
>
{tickets.map((ticket) => (
<TicketRow key={ticket.ticketId} ticket={ticket} />
))}
</ul>
)}
</li> </li>
); );
} }

View File

@ -55,6 +55,7 @@ export function useProjectWorkState(projectId: string): ProjectWorkStateViewMode
event.type === "agentLaunched" || event.type === "agentLaunched" ||
event.type === "agentExited" || event.type === "agentExited" ||
event.type === "agentBusyChanged" || event.type === "agentBusyChanged" ||
event.type === "delegationReady" ||
event.type === "orchestratorRequestProcessed" event.type === "orchestratorRequestProcessed"
) { ) {
void refresh(); void refresh();

View File

@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen, waitFor, within } from "@testing-library/react";
import { DIProvider } from "@/app/di"; import { DIProvider } from "@/app/di";
import { MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock"; import { MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock";
@ -49,6 +49,7 @@ describe("ProjectWorkStatePanel", () => {
expect(await screen.findByText("Planner")).toBeTruthy(); expect(await screen.findByText("Planner")).toBeTruthy();
expect(screen.getByText("Offline")).toBeTruthy(); expect(screen.getByText("Offline")).toBeTruthy();
expect(screen.getByText("Idle")).toBeTruthy(); expect(screen.getByText("Idle")).toBeTruthy();
expect(screen.queryByLabelText("Planner tickets")).toBeNull();
}); });
it("renders a live busy agent with a short ticket", async () => { it("renders a live busy agent with a short ticket", async () => {
@ -81,6 +82,116 @@ describe("ProjectWorkStatePanel", () => {
expect(screen.getByLabelText("busy ticket 4c65d981")).toBeTruthy(); expect(screen.getByLabelText("busy ticket 4c65d981")).toBeTruthy();
}); });
it("renders an in-progress human ticket", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-4",
name: "Implementer",
profileId: "codex",
busy: {
state: "busy",
ticket: "ticket-in-progress-abcdef",
sinceMs: 789,
},
tickets: [
{
ticketId: "ticket-in-progress-abcdef",
conversationId: "conversation-a",
position: 0,
status: "inProgress",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: "Wire the workstate queue UI",
taskLen: 36,
},
],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Implementer tickets");
expect(within(list).getByText("#1 In progress")).toBeTruthy();
expect(within(list).getByText("Human")).toBeTruthy();
expect(within(list).getByText("Wire the workstate queue UI")).toBeTruthy();
expect(within(list).getByLabelText("ticket ticket-i")).toBeTruthy();
expect(within(list).getByLabelText("9 more characters")).toBeTruthy();
});
it("renders two tickets in FIFO order", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-5",
name: "Reviewer",
profileId: "claude",
busy: { state: "idle" },
tickets: [
{
ticketId: "ticket-second-abcdef",
conversationId: "conversation-b",
position: 1,
status: "queued",
source: { kind: "agent", agentId: "agent-main-123456" },
requesterLabel: "Main",
taskPreview: "Then review the result",
taskLen: 22,
},
{
ticketId: "ticket-first-abcdef",
conversationId: "conversation-a",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: "agent-qa-123456" },
requesterLabel: "QA",
taskPreview: "Check behavior",
taskLen: 14,
},
],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Reviewer tickets");
expect(within(list).getByText("QA (agent-qa)")).toBeTruthy();
expect(within(list).getByText("Main (agent-ma)")).toBeTruthy();
expect(list.textContent?.indexOf("#1 In progress")).toBeLessThan(
list.textContent?.indexOf("#2 Queued") ?? -1,
);
});
it("normalizes mock work state without tickets", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-6",
name: "Legacy",
profileId: "gemini",
busy: { state: "idle" },
},
],
});
await expect(workState.getProjectWorkState(PROJECT_ID)).resolves.toEqual({
agents: [
{
agentId: "agent-6",
name: "Legacy",
profileId: "gemini",
busy: { state: "idle" },
tickets: [],
},
],
});
});
it("refreshes when a relevant domain event fires", async () => { it("refreshes when a relevant domain event fires", async () => {
const workState = new MockWorkStateGateway(); const workState = new MockWorkStateGateway();
const system = new MockSystemGateway(); const system = new MockSystemGateway();
@ -108,4 +219,46 @@ describe("ProjectWorkStatePanel", () => {
expect(await screen.findByText("Responder")).toBeTruthy(); expect(await screen.findByText("Responder")).toBeTruthy();
await waitFor(() => expect(spy).toHaveBeenCalled()); await waitFor(() => expect(spy).toHaveBeenCalled());
}); });
it("refreshes when delegationReady fires", async () => {
const workState = new MockWorkStateGateway();
const system = new MockSystemGateway();
renderPanel(workState, system);
await screen.findByText("No agent work state.");
const spy = vi.spyOn(workState, "getProjectWorkState");
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-7",
name: "Delegatee",
profileId: "codex",
busy: { state: "busy", ticket: "ticket-ready-123456", sinceMs: 123 },
tickets: [
{
ticketId: "ticket-ready-123456",
conversationId: "conversation-ready",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: "agent-main-123456" },
requesterLabel: "Main",
taskPreview: "Handle this delegated task",
taskLen: 26,
},
],
},
],
});
system.emit({
type: "delegationReady",
agentId: "agent-7",
ticket: "ticket-ready-123456",
text: "Handle this delegated task",
});
expect(await screen.findByText("Delegatee")).toBeTruthy();
expect(screen.getByText("#1 In progress")).toBeTruthy();
await waitFor(() => expect(spy).toHaveBeenCalled());
});
}); });