docs(qw): spec design QW suite mai 2026 (multi-écrans + LoopDetector + safety_checks hybrides)

Spec issu d'un brainstorming structuré (7 questions clarifiantes,
décisions tranchées) inspiré par l'exploration comparative de 5 frameworks
computer-use (Simular Agent-S, browser-use, OpenAI CUA sample, Coasty
open-cu, Showlab OOTB).

3 quick wins ciblés :
- QW1 multi-écrans : capture/grounding par monitor_index avec fallbacks
- QW2 LoopDetector composite : screen_static (CLIP) + action_repeat + retry
- QW4 safety_checks hybrides : déclaratif workflow + LLM contextuel
  (medgemma:4b, timeout 5s, fallback safe, kill-switch env)

Contraintes inviolables : 100% vision, 100% local Ollama, backward compat.
Plan livraison : QW1+QW2 avant démo GHT, QW4 enchaîné dès validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-05 22:23:10 +02:00
parent 35b27ae492
commit 2a07d8084b

View File

@@ -0,0 +1,467 @@
# Spec — QW Suite Mai 2026
| Champ | Valeur |
|---|---|
| Date | 2026-05-05 |
| Auteur | Dom + Claude (brainstorming structuré) |
| Branche | `feature/qw-suite-mai` (depuis `feature/feedback-bus`) |
| Backup | `backup/pre-qw-suite-mai-2026-05-05` à pousser sur Gitea avant 1er commit |
| Statut | Design approuvé — spec à valider par Dom avant `writing-plans` |
| Cibles démo | GHT Sud 95 (1ère sem mai 2026, date à confirmer) |
| Contraintes inviolables | 100% vision · 100% local (Ollama) · backward compatible |
## 1. Contexte & motivation
Suite à l'exploration comparative de 5 frameworks computer-use (Simular Agent-S, browser-use, OpenAI CUA sample, Coasty open-cu, Showlab OOTB), trois quick wins ont été identifiés comme améliorations à fort ratio valeur/risque pour RPA Vision V3, alignés avec la philosophie du projet (vision pure, souveraineté, supervision médicale) :
- **QW1 — Multi-écrans propre** (inspiré OOTB) : capture et grounding sur l'écran cible plutôt que sur le composite tous écrans. Gain de perf grounding + correction des coordonnées.
- **QW2 — LoopDetector composite** (inspiré browser-use) : détecter quand Léa exécute des actions techniquement valides mais que l'écran ne progresse pas, et escalader vers l'humain plutôt que de tourner en rond muettement.
- **QW4 — Safety checks hybrides** (inspiré OpenAI CUA + browser-use Pydantic registry) : enrichir l'action `pause_for_human` avec une liste de vérifications à acquitter, mêlant déclaratif (workflow) et contextuel (LLM local).
Effet cumulé attendu : Léa devient observable, robuste et auditable sans rien céder sur le 100% local.
## 2. Décisions de design (récap)
| Sujet | Décision |
|---|---|
| Activation | Default-ON pour tous les workflows (Dom recréera ce qui en a besoin) |
| QW1 — Stratégie ciblage écran | `monitor_index` enregistré à la capture → fallback focus actif → fallback composite (backward) |
| QW1 — Niveau de stack | Client Agent V1 (capture) + serveur (routeur) + `core/execution/input_handler.py` (capture locale) |
| QW2 — Signal de boucle | Composite OR : screen_static (CLIP) + action_repeat + retry_threshold |
| QW2 — Sortie | `replay_state["status"] = "paused_need_help"` avec `pause_reason` structuré |
| QW4 — Source des checks | Hybride : déclaratif workflow + LLM contextuel sur `safety_level: "medical_critical"` |
| QW4 — Robustesse LLM | `medgemma:4b` + timeout 5s + `format=json` Ollama + JSON Schema strict + fallback safe (zéro check additionnel) + kill-switch env var |
| QW4 — UX VWB | Bulle existante préservée + `<ChecklistPanel>` au-dessus de Continuer (bouton désactivé tant que required non cochés) |
| Ordre de livraison | QW1 → QW2 → QW4 (du moins invasif au plus visible) |
| Plan timing | Option A : QW1+QW2 avant démo ; QW4 enchaîné dès validation des deux premiers |
| Kill-switches | Env vars sur QW2 et QW4, surchargeables par `systemctl edit` |
| Backward compatibility | 100% — aucun champ obligatoire ajouté au DSL ; workflows existants se comportent comme avant |
## 3. Architecture globale
```
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ Agent V1 (Windows) │ │ Serveur Streaming (5005) │
│ │ │ │
│ ┌──────────────────┐ │ │ ┌───────────────────────────┐ │
│ │ ScreenCapture │ │ │ │ LoopDetector [QW2] │ │
│ │ + monitor_index │───┼────────▶│ │ • screen_static (CLIP) │ │
│ │ [QW1] │ │ HTTP │ │ • action_repeat │ │
│ └──────────────────┘ │ │ │ • retry_threshold │ │
│ │ │ │ → paused_need_help │ │
│ ┌──────────────────┐ │ │ └───────────────────────────┘ │
│ │ FeedbackBus lea:*│◀──┼─────────┤ │
│ │ chat_window │ │ │ ┌───────────────────────────┐ │
│ └──────────────────┘ │ │ │ SafetyChecksProvider │ │
└─────────────────────────┘ │ │ [QW4] │ │
│ │ • declarative (workflow) │ │
│ │ • LLM contextual │ │
│ │ medgemma:4b 5s/JSON │ │
│ │ fallback safe │ │
│ │ • kill-switch env var │ │
│ └───────────────────────────┘ │
│ │
│ ┌───────────────────────────┐ │
│ │ MonitorRouter [QW1] │ │
│ │ • cible monitor_index │ │
│ │ • fallback focus actif │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ VWB Frontend (3002) │
│ │
│ PauseDialog (étendu) [QW4-UX] │
│ • bulle existante préservée │
│ • + ChecklistPanel │
│ (cases à cocher acquittables)│
│ • + pause_reason si loop │
│ Continuer désactivé tant que │
│ required-checks non cochés │
└─────────────────────────────────┘
```
### Principes invariants
1. Aucun nouveau service, aucune nouvelle DB. Tout dans la stack existante (Agent V1 + serveur 5005 + VWB 3002).
2. 3 modules serveur isolés (`monitor_router.py`, `loop_detector.py`, `safety_checks_provider.py`) — couplage faible, testables individuellement, désactivables par env var.
3. Backward compatible : workflows sans nouveaux champs se comportent comme avant.
4. Kill-switches env vars sur QW2 et QW4, override possible via `systemctl edit` pendant la démo.
5. 100% vision : QW1 pure capture + grounding ; QW2 réutilise le `_clip_embedder` déjà chargé ; QW4 LLM = Ollama local strict.
6. Bus `lea:*` étendu de 4 events d'observabilité : `lea:loop_detected`, `lea:safety_checks_generated`, `lea:safety_checks_llm_failed`, `lea:monitor_routed`.
### Surface de modification (ordre A)
| QW | Fichiers nouveaux | Fichiers modifiés |
|---|---|---|
| QW1 | `agent_v0/server_v1/monitor_router.py` | `agent_v0/agent_v1/capture/screen_capture.py`, `core/execution/input_handler.py`, `agent_v0/server_v1/api_stream.py` (~10 lignes) |
| QW2 | `agent_v0/server_v1/loop_detector.py` | `agent_v0/server_v1/replay_engine.py` (~30 lignes), `agent_v0/server_v1/api_stream.py` (~20 lignes) |
| QW4 | `agent_v0/server_v1/safety_checks_provider.py`, `visual_workflow_builder/frontend_v4/src/components/PauseDialog.tsx` | `agent_v0/server_v1/replay_engine.py`, `agent_v0/server_v1/api_stream.py` (`/replay/resume`), `visual_workflow_builder/frontend_v4/src/types.ts`, `visual_workflow_builder/frontend_v4/src/components/PropertiesPanel.tsx` |
## 4. QW1 — Multi-écrans
### 4.1 Composants
**Client Agent V1**`agent_v0/agent_v1/capture/screen_capture.py` (existant à modifier)
- Enrichit chaque heartbeat / event avec :
- `monitor_index: int`
- `monitors_geometry: [{idx, x, y, w, h, primary}]`
- Détection via `screeninfo` (port direct depuis Showlab OOTB)
- Capture de l'écran *actif uniquement* (poids réseau identique à aujourd'hui)
- Si `screeninfo` indisponible côté Windows : envoie `monitors_geometry: []`, comportement composite préservé
**Serveur** — nouveau `agent_v0/server_v1/monitor_router.py` (~80 lignes)
- API : `resolve_target_monitor(action: dict, session_state: dict) → MonitorTarget`
- `MonitorTarget = {idx, offset_x, offset_y, w, h, source: "action" | "focus" | "composite_fallback"}`
- Stratégie :
1. Lit `action.get("monitor_index")` si présent → cible cet écran
2. Sinon `session_state.get("last_focused_monitor")` → cible focus actif
3. Sinon `monitors[0]` composite (comportement actuel — backward)
**Input local Linux**`core/execution/input_handler.py` modifs ciblées
- Signature changée : `_capture_screen(monitor_idx=None) → (image, w, h, offset_x, offset_y)`
- Quand `monitor_idx` fourni : capture uniquement ce monitor
- Toutes les fonctions `_grounding_*` (`_grounding_ocr`, `_grounding_ui_tars`, `_grounding_vlm`) propagent l'offset pour traduire les coords retournées en coords absolues écran
### 4.2 Data flow replay
```
Action [monitor_index=1] reçue par serveur
→ MonitorRouter.resolve()
→ target_monitor = {idx:1, offset:(1920,0), w:1920, h:1080, source:"action"}
→ grounding capture monitor 1 uniquement (image 1920×1080, pas 3840×1080)
→ UI-TARS / OCR / VLM cherche cible → coords locales (640, 540)
→ coords absolues = (640+1920, 540+0) = (2560, 540)
→ pyautogui.click(2560, 540)
→ bus.emit("lea:monitor_routed", {idx:1, source:"action"})
```
### 4.3 Error handling
| Cas | Comportement |
|---|---|
| `monitor_index` absent (vieille session) | Fallback focus actif, log info `lea:monitor_routed source=focus` |
| Monitor enregistré n'existe plus (2nd écran débranché) | Fallback focus actif, event `lea:monitor_unavailable` warning |
| `mss.monitors[i]` hors limites | Fallback `monitors[0]` composite, event `lea:monitor_invalid_index` error |
| `screeninfo` non installé côté Agent V1 | `monitors_geometry: []`, fallback composite (comportement actuel) — pas de blocage |
### 4.4 Tests QW1
- `tests/unit/test_monitor_router.py` : 4 cas (cible OK, fallback focus, fallback composite, monitor débranché)
- `tests/integration/test_grounding_offset.py` : capture 1 monitor + clic résolu avec offset (mock pyautogui)
- Smoke : 1 workflow Easily rejoué, vérification visuelle que le clic atterrit au bon endroit
### 4.5 Compat workflows existants
Aucune action n'a `monitor_index` aujourd'hui → 100% des workflows existants partent en fallback focus actif → comportement quasi-identique au composite actuel mais sur un seul écran (gain de perf grounding même sans recréation de workflow).
## 5. QW2 — LoopDetector composite
### 5.1 Composants
**Nouveau** `agent_v0/server_v1/loop_detector.py` (~150 lignes)
- Classe `LoopDetector` avec 3 sous-détecteurs
- API : `evaluate(replay_state, screenshot_history, action_history) → LoopVerdict`
- `LoopVerdict = {detected: bool, reason: str, signal: str, evidence: dict}`
**Hook** dans `agent_v0/server_v1/api_stream.py`
- Après chaque `report_action_result`, appel `loop_detector.evaluate(...)` si `RPA_LOOP_DETECTOR_ENABLED=1` (défaut)
- Si `verdict.detected` :
- `replay_state["status"] = "paused_need_help"`
- `replay_state["pause_reason"] = verdict.reason`
- `replay_state["pause_message"] = f"Léa semble bloquée — {verdict.signal}"`
- bus.emit `lea:loop_detected` avec `{signal, evidence, replay_id}`
**Étendu** dans `replay_engine.py` :
- `_create_replay_state()` ajoute :
- `"_screenshot_history": []` (anneau de 5 derniers embeddings CLIP)
- `"_action_history": []` (anneau des 5 dernières actions)
- `_pre_check_screen_state()` continue indépendamment (signal différent : check pré-action vs détection post-action de stagnation)
### 5.2 Signaux composites
| Signal | Détecteur | Seuil par défaut | Source |
|---|---|---|---|
| `screen_static` | A | 4 captures consécutives avec CLIP similarity > 0.99 | `_clip_embedder` déjà chargé serveur |
| `action_repeat` | B | 3 actions consécutives identiques (type + coords) | `_action_history` |
| `retry_threshold` | C | 3 retries sur même `action_id` | `replay_state["retried_actions"]` (déjà existant) |
Un seul signal positif suffit à déclencher l'escalade.
### 5.3 Data flow
```
Action exécutée → result reçu via /replay/result
LoopDetector.evaluate(state, screenshots, actions) si RPA_LOOP_DETECTOR_ENABLED=1
├─ A.check_screen_static() → embed(latest), compare aux N-1 derniers
├─ B.check_action_repeat() → compare action_history[-3:]
└─ C.check_retry_threshold() → state["retried_actions"] >= 3
Si verdict.detected:
state["status"] = "paused_need_help"
state["pause_reason"] = verdict.reason
state["pause_message"] = f"Léa semble bloquée — {verdict.signal} ({evidence})"
bus.emit("lea:loop_detected", {signal, evidence, replay_id})
```
### 5.4 Error handling
| Cas | Comportement |
|---|---|
| CLIP embedder unavailable | Signal A désactivé (warning log 1×), B+C continuent. Pas de blocage. |
| `_screenshot_history` < N | Signal A skip silencieusement (pas assez d'historique) |
| `embed_image()` lève une exception | Catch + log warning, replay continue (verdict = `detected=False`) |
| `RPA_LOOP_DETECTOR_ENABLED=0` | Module entier bypassé, comportement antérieur |
| Faux positif détecté en pleine démo | `RPA_LOOP_DETECTOR_ENABLED=0` via `systemctl edit rpa-streaming` + restart → reprise immédiate |
### 5.5 Configuration env vars
- `RPA_LOOP_DETECTOR_ENABLED=1` (défaut)
- `RPA_LOOP_SCREEN_STATIC_THRESHOLD=0.99`
- `RPA_LOOP_SCREEN_STATIC_N=4`
- `RPA_LOOP_ACTION_REPEAT_N=3`
- `RPA_LOOP_RETRY_THRESHOLD=3`
### 5.6 Tests QW2
- `tests/unit/test_loop_detector.py` : 8 cas (chaque signal isolé, chaque combinaison, kill-switch, embedder absent)
- `tests/integration/test_loop_detector_replay.py` : 3 cas — replay simulé qui boucle → vérifier transition `running → paused_need_help` avec bonne raison
- Pas de smoke démo (impossible à reproduire fiable, on s'appuie sur les tests intégration)
### 5.7 Compat VWB
Aucune côté frontend pour QW2 : la pause `paused_need_help` existe déjà. Le `pause_reason` enrichi sera affiché par le composant `PauseDialog` étendu en QW4. Avant la livraison de QW4, la raison s'affichera en texte dans le `pause_message` (donc utile dès le commit QW2).
## 6. QW4 — Safety checks hybrides
### 6.1 Contrat de l'action étendue (rétro-compatible)
```json
{
"type": "pause_for_human",
"parameters": {
"message": "Validation T2A avant codage",
"safety_level": "medical_critical",
"safety_checks": [
{"id": "check_ipp", "label": "Vérifier IPP patient", "required": true},
{"id": "check_cim10", "label": "Confirmer code CIM-10", "required": true}
]
}
}
```
`safety_level` et `safety_checks` sont **optionnels**. Action sans ces champs → comportement actuel (bulle simple, aucun appel LLM).
### 6.2 Composants serveur
**Nouveau** `agent_v0/server_v1/safety_checks_provider.py` (~180 lignes)
- API : `build_pause_payload(action, replay_state, last_screenshot) → PausePayload`
- Concatène : checks déclaratifs (workflow) + checks contextuels (LLM si `safety_level == "medical_critical"`)
- Chaque check porte sa source : `source: "declarative" | "llm_contextual"` et son `evidence` (vide pour déclaratif, justification courte pour LLM)
- Format check final :
```json
{
"id": "check_xxx",
"label": "...",
"required": true,
"source": "declarative" | "llm_contextual",
"evidence": null | "..."
}
```
**LLM contextual call** — sous-fonction `_call_llm_for_contextual_checks()`
- Modèle : `medgemma:4b` (env `RPA_SAFETY_CHECKS_LLM_MODEL`)
- Timeout dur : 5s (env `RPA_SAFETY_CHECKS_LLM_TIMEOUT_S`)
- `format=json` natif Ollama + JSON Schema strict :
```json
{"additional_checks": [{"label": "string", "evidence": "string"}]}
```
- Max 3 checks ajoutés (env `RPA_SAFETY_CHECKS_LLM_MAX_CHECKS`)
- Prompt : screenshot heartbeat actuel + workflow message + liste des checks déclaratifs (évite doublons)
- Tout échec (timeout, exception, JSON invalide post-schema) → `additional_checks = []`, event `lea:safety_checks_llm_failed`, replay continue
**Hook** dans `replay_engine.py` — branche `action_type == "pause_for_human"`
- Avant de basculer en `paused_need_help`, appel `safety_checks_provider.build_pause_payload(...)`
- Stocke `replay_state["safety_checks"] = payload.checks`
- Stocke `replay_state["pause_payload"] = payload` (pour debug/audit)
**Modif** `api_stream.py` — endpoint `/replay/resume`
- Reçoit `{acknowledged_check_ids: [...]}` dans le body POST
- Vérifie : tous les checks `required=true` doivent être dans `acknowledged_check_ids`
- Sinon : `400 {error: "required_checks_missing", missing: [...]}`
- Stocke `replay_state["checks_acknowledged"] = acknowledged_check_ids` (audit trail)
- Reprise normale du replay
### 6.3 Composants frontend VWB
**Nouveau** `visual_workflow_builder/frontend_v4/src/components/PauseDialog.tsx` (~200 lignes)
- Props : `pauseMessage`, `pauseReason`, `safetyChecks`, `onResume(ackIds)`, `onCancel`
- Si `safetyChecks.length === 0` : rend la bulle existante (legacy, comportement actuel)
- Sinon : bulle + `<ChecklistPanel>` avec checkboxes
- Bouton Continuer disabled tant que `checks.filter(c => c.required && !checked).length > 0`
- POST `/replay/resume` avec body `{acknowledged_check_ids: [...]}`
- Visuel source :
- Badge `[Léa]` pour `source: "llm_contextual"` (avec tooltip `evidence`)
- Badge `[obligatoire]` pour `required: true`
**Étendu** `types.ts`
- `PauseAction['parameters']` : ajout `safety_level?`, `safety_checks?`
- `Execution` : ajout `pause_reason?`, `safety_checks?`
**Étendu** `PropertiesPanel.tsx:1356` — éditeur de l'action `pause_for_human`
- Section "Niveau de sécurité" : dropdown `standard | medical_critical`
- Section "Checks à valider" : liste éditable (id + label + required)
### 6.4 Data flow complet
```
Action pause_for_human (medical_critical, 2 checks déclaratifs) atteinte
SafetyChecksProvider.build_pause_payload()
├─ checks = [...declarative] (2 entrées)
├─ if safety_level == "medical_critical" and RPA_SAFETY_CHECKS_LLM_ENABLED=1:
│ llm_checks = _call_llm_for_contextual_checks() (max 3, timeout 5s)
│ checks += llm_checks
└─ return PausePayload(checks, pause_reason, message)
replay_state["status"] = "paused_need_help"
replay_state["safety_checks"] = checks
bus.emit("lea:safety_checks_generated", {count, sources})
Frontend VWB poll /replay/state → reçoit pause_payload
<PauseDialog> rend ChecklistPanel
Médecin coche les 4 checks → clique Continuer
POST /replay/resume {acknowledged_check_ids: [4 ids]}
Serveur valide (tous required acquittés) → reprise du replay
replay_state["checks_acknowledged"] = [...] (audit trail conservé)
```
### 6.5 Error handling
| Cas | Comportement |
|---|---|
| `safety_level` absent | Pas d'appel LLM ; checks déclaratifs uniquement (peut être `[]`) → bulle simple si vide, checklist sinon |
| Ollama timeout 5s | Event `lea:safety_checks_llm_failed`, `additional_checks=[]`, fallback safe (déclaratifs seuls) |
| Ollama JSON malformé (post `format=json` — théoriquement impossible) | Idem timeout, fallback safe |
| LLM produit un check absurde | Accepté tel quel, le superviseur ignore (pas de filtrage en V1) |
| Frontend reçoit `safety_checks=[]` | Bulle simple, comportement legacy |
| `RPA_SAFETY_CHECKS_LLM_ENABLED=0` | Couche LLM bypassée, déclaratifs gardés |
| `/replay/resume` sans `acknowledged_check_ids` sur required | `400 required_checks_missing` |
| Frontend POST `/replay/resume` rejeté | Toast d'erreur côté UI, état pause conservé, possibilité de cocher manquants et réessayer |
### 6.6 Configuration env vars
- `RPA_SAFETY_CHECKS_LLM_ENABLED=1` (défaut)
- `RPA_SAFETY_CHECKS_LLM_MODEL=medgemma:4b`
- `RPA_SAFETY_CHECKS_LLM_TIMEOUT_S=5`
- `RPA_SAFETY_CHECKS_LLM_MAX_CHECKS=3`
### 6.7 Tests QW4
- `tests/unit/test_safety_checks_provider.py` : 7 cas (déclaratif seul, hybride réussi, LLM timeout, LLM JSON invalide, kill-switch, max_checks respecté, déclaratif vide)
- `tests/integration/test_replay_resume_acknowledgments.py` : 3 cas (resume OK, missing required → 400, audit trail enregistré dans `checks_acknowledged`)
- Frontend : `tests/components/PauseDialog.test.tsx` si suite Vitest existe (à confirmer pendant l'implémentation), sinon test manuel avec checklist écrite
- Smoke : 1 workflow Easily avec `pause_for_human medical_critical` enrichi → vérification full chain
### 6.8 Compat workflows existants
100% backward — `pause_for_human` actuels n'ont ni `safety_level` ni `safety_checks` → comportement strictement identique. Aucune recréation forcée. Dom enrichira uniquement les workflows qu'il veut promouvoir au niveau `medical_critical`.
## 7. Tests, sécurité de la branche, livraison
### 7.1 Filet de sécurité avant TOUT commit sur `feature/qw-suite-mai`
1. Branche backup poussée Gitea : `backup/pre-qw-suite-mai-2026-05-05`
2. Capture baseline E2E :
```
pytest tests/test_pipeline_e2e.py \
tests/test_phase0_integration.py \
tests/integration/test_stream_processor.py \
-q 2>&1 | tee .qw-baseline.log
```
3. Smoke démo : 1 dérouler complet d'un workflow Easily Assure, archivage screenshot/vidéo de référence
4. État VWB validé : démarrage Vite local, ouverture d'un workflow, lancement d'un replay simple, screenshot "tout va bien"
### 7.2 Discipline TDD légère par QW
- Test unitaire écrit AVANT le code de production (1 test rouge → 1 implémentation → vert)
- Pas de TDD complet sur le frontend (Vitest + React = trop d'outillage à valider en parallèle), test manuel cadré avec checklist écrite
- Re-run de la suite baseline après chaque commit QW, comparaison au log archivé
- Toute régression bloque le passage au QW suivant tant qu'elle n'est pas comprise et résolue
### 7.3 Compat VWB — checklist explicite avant commit QW4
- [ ] Workflow ancien (sans `safety_checks`) → bulle simple s'affiche normalement
- [ ] Workflow nouveau avec `safety_checks` déclaratifs uniquement → checklist visible, **pas** d'appel Ollama (vérification logs)
- [ ] Workflow `medical_critical` → checklist + checks LLM apparaissent (vérification logs Ollama call dans les 5s)
- [ ] Continuer désactivé tant que required non cochés
- [ ] POST `/replay/resume` avec mauvais payload → toast d'erreur côté UI, pas de crash
- [ ] PropertiesPanel : édition de `safety_checks` ne casse pas l'édition d'autres params de `pause_for_human`
- [ ] DB `workflows.db` : ouverture après commit, aucune migration cassante (schéma JSON est libre)
### 7.4 Plan de commits
```
1. test(qw1): tests monitor_router + grounding_offset (rouges)
2. feat(qw1): multi-écrans piloté par monitor_index (verts)
3. test(qw2): tests loop_detector composite (rouges)
4. feat(qw2): LoopDetector composite avec kill-switch env
5. test(qw4): tests safety_checks_provider + replay_resume (rouges)
6. feat(qw4): safety_checks hybride déclaratif + LLM contextuel
7. feat(vwb): PauseDialog + ChecklistPanel + extension PropertiesPanel
8. docs(qw): docs/QW_SUITE_MAI.md + mise à jour MEMORY.md
```
Chaque commit signé Co-Authored-By Claude. Branche poussée régulièrement sur Gitea pour backup distant.
### 7.5 Stratégie en cas de régression critique pendant la démo
Kill-switches env vars surchargeables sans redéploiement code :
```
systemctl edit rpa-streaming
# Ajouter sous [Service] :
Environment=RPA_LOOP_DETECTOR_ENABLED=0
Environment=RPA_SAFETY_CHECKS_LLM_ENABLED=0
systemctl restart rpa-streaming
```
Si problème grave au-delà des kill-switches : rollback à `backup/pre-qw-suite-mai-2026-05-05`.
```
git checkout backup/pre-qw-suite-mai-2026-05-05
./svc.sh restart
```
### 7.6 Plan de livraison (Option A validée)
**Avant démo GHT (cette semaine) — Sprint priorité 1**
- QW1 : tests + code + smoke (~1j)
- QW2 : tests + code + tests intégration (~2j)
- Capture baseline + replay smoke entre chaque
- Si QW1+QW2 validés et probants → on enchaîne sur QW4 dès que possible (Dom accepte le weekend si "effet waouh" auprès de spécialistes RPA)
**Après démo / dès validation QW1+QW2 — Sprint priorité 2**
- QW4 serveur (provider + LLM + endpoint resume) (~3j)
- QW4 frontend (PauseDialog + PropertiesPanel) (~2j)
- Doc + mise à jour MEMORY.md
**Total estimé** : ~8.5j-h ingénieur senior, étalable selon le retour démo.
## 8. Ce qui n'est PAS dans ce spec (out of scope)
- F1 (DSL d'actions Pydantic-first) : refactor de fond, sera son propre spec après la démo.
- F2 (Mixture-of-Grounding routeur adaptatif) : nécessite F1, son propre spec.
- F3 (Best-of-N + Reflection) : nécessite F1, son propre spec.
- QW3 (`output_model_schema` Pydantic pour `extract_text`) : opportuniste, sera intégré quand on touchera `extract_text` pour autre chose.
- Toute introduction de Pydantic-AI / instructor / Playwright / accessibility-tree : interdit (contraintes inviolables).
- Refonte du composant pause en `<PauseDialog>` à 3 modes (option C de Q6) : reportée après démo si retour utilisateurs justifie l'investissement.
## 9. Open questions
Aucune. Toutes les décisions de design ont été tranchées via les 7 questions clarifiantes du brainstorming du 5 mai 2026.