Field notes
Every friction met while using Diwall for real work, numbered and dated since May 2026 — including the ones caused by our own mistakes.
Contents
Since the first sessions, every friction met while doing real work with Diwall has been written down: what was attempted, what happened, why, and what was done about it. More than a hundred entries, numbered and dated.
They are the raw material of the recipes — a recipe is a friction that has been solved often enough to be worth generalising.
What goes in
Not only the frictions caused by Diwall. The log also holds:
Our own mistakes. A password for a local development application, left in the public history for weeks because it guarded nothing and seemed harmless at the time. A synthetic fixture substituted for the specified one and presented as conformant. Both recorded with the same detail as any tool defect.
Frictions that turned out not to be Diwall’s. A synchronisation problem blamed on the tool for two sessions before the real cause surfaced elsewhere. The wrong diagnosis stays in the log next to the right one, because the wrong diagnosis is what cost the time.
Things that worked, and why. An encrypted directory so transparent the operator never noticed it. An accessibility tree sufficient for a whole crawl with no vision model called.
The rule that makes it usable
An entry is written when the friction is met, not when it is resolved.
Written afterwards, a friction becomes a tidy story with a clean cause. Written during, it keeps the false leads — and the false leads are what a reader recognises when they are stuck at the same point.
That is also why some entries end without a resolution. A friction that is still open is more honest as an open entry than as a silence.
These notes are in French
They are a French operator’s field log, written in the language the work was done in. Translating them would smooth the register — a field note is idiomatic, sometimes blunt, and a machine translation of it reads like a report.
They stay in their original language, and translations, when they exist, are marked as transcriptions rather than presented as equivalents. The day a German-speaking operator writes their own, the same rule applies in reverse.
What it is for
For us: not solving the same problem twice, and knowing which limits are real.
For you: knowing what this tool actually costs, including the days when nothing works, before committing to it.
The log itself follows, in French, exactly as it was written.
1. Le piège de Mode B (sessions ReAct)
Au départ, j’ai cru qu’il fallait --sauver-session / --reprendre-session pour
tout parcours multi-étapes. Réflexe naturel : « ReAct = un pas à la fois ».
Réalité : Mode B coûte cher — chaque appel relance Playwright, recharge le storage_state, redessine le SoM. Pour un formulaire de 3 champs + submit, c’est 4 captures × ~2 s = 8 s minimum, plus le temps LLM entre chaque pas.
Ce qui marche : --action accepte un tableau d’actions atomiques. Une
seule invocation enchaîne fill+fill+fill+click, une seule capture finale.
--action '[
{"type":"remplir_som","id":1,"valeur":"foo"},
{"type":"remplir_som","id":2,"valeur":"bar"},
{"type":"cliquer_som","id":3}
]'
Heuristique : Mode B uniquement quand le DOM change entre deux pas et que je dois voir ce changement pour décider du suivant.
2. --output sans extension → mime type None
J’ai passé --output /tmp/test (sans .png). Erreur peu parlante :
unknown mime type None. Quelques minutes perdues à chercher si c’était un
problème Playwright, un PATH, une perm.
Origine : Playwright déduit le format d’image de l’extension du chemin de sortie. Pas d’extension, pas de mime type.
Fix côté utilisateur : toujours .png (ou laisser Diwall générer le nom
automatique dans /tmp/diwall/).
Fix côté Diwall (suggestion) : ajouter .png si manquant, ou échouer tôt
avec un message explicite.
3. remplir plante quand le sélecteur matche plusieurs éléments
Tentative naïve : {"type":"remplir","selecteur":"input","valeur":"foo"}.
Le formulaire a 3 inputs → Playwright refuse (strict mode violation).
Lecon : remplir (CSS) est piégeux dès qu’il y a plusieurs candidats.
Le SoM résout ça nativement — chaque élément a un ID unique.
Règle perso : préférer remplir_som par défaut, garder remplir pour les
cas où l’on sait que le sélecteur est unique (ex : #username).
4. remplir_som sur input pré-rempli : le triple-clic rate
Le formulaire avait un champ URL pré-rempli avec https://. J’ai voulu écrire
https://ada.example.com. Diwall a tapé ada.example.com après le contenu
existant → résultat : https://ada.example.com (par chance correct au premier
test), mais sur le troisième champ URL, le triple-clic de sélection n’a pas
pris → le préfixe est resté → domaine créé = ada au lieu de ada.example.com.
Origine : remplir_som fait triple-click → fill. Sur certains inputs
(autocomplete, masques), le triple-click ne sélectionne pas tout le contenu.
Workaround : précéder le fill d’un clear explicite, ou utiliser
element.fill('') puis element.fill(value) côté Playwright.
Fix côté Diwall (suggestion) : faire element.fill('') avant le fill
réel, plutôt que de compter sur le triple-click.
5. Session PHP non persistée entre appels Mode B
Login réussi → capture montre la page authentifiée. Action suivante en
--reprendre-session → redirection vers /login. Cookies perdus.
Origine : storage_state capture cookies + localStorage, mais
pas la session côté serveur. Si le serveur PHP refait session_start()
avec un session ID périmé (ex : régénéré post-login), le cookie côté client
n’aide pas.
Dans le cas Sillage, c’était plus subtil : session_regenerate_id() après
login changeait l’ID, et --sauver-session capturait l’ancien. La fenêtre
de validité du nouveau cookie n’était pas dans le snapshot.
Workaround : enchaîner login + action suivante en un seul Mode A multi-actions. Ou refaire le login à chaque étape (coûteux).
Mention dans GUIDE_LLM.md : la note SPA §3 dit
« Form values are NOT persisted in storage_state » — c’est vrai, mais
le problème session-côté-serveur mériterait sa propre ligne.
6. button:has-text() — la syntaxe Playwright qui n’est pas du CSS
Réflexe : {"selecteur":"button:has-text('Connexion')"}. C’est de la syntaxe
Playwright (pseudo-sélecteur étendu), pas du CSS standard. Diwall passe ça
tel quel à Playwright → ça marche la plupart du temps, mais ce n’est pas
documenté dans GUIDE_LLM.md.
Suggestion doc : ajouter au tableau des actions une ligne
« Playwright extended selectors supportés (:has-text(), :visible, etc.) »
ou au contraire les interdire pour rester strictement CSS.
Synthèse session 1 — ce qui m’aurait fait gagner du temps
- Mode A multi-actions par défaut, Mode B comme exception → à dire plus
fort dans
GUIDE_LLM.md. La doc actuelle présente Mode B en premier (§78), ce qui suggère que c’est le mode normal. - Coup d’œil dès le départ sur les pièges du SoM — pré-remplis, autocomplete, inputs maskés. Une section « SoM edge cases » manque.
- Un exemple de scénario complexe (login + form + submit) dans
examples/aiderait à voir le pattern « tout en une action ».
Le reste — l’idée d’avoir des yeux, la boucle Read(PNG) → analyse → fix,
le coût marginal d’une capture, la rapidité du SoM vs cliquer_visuel —
c’est aussi bon que la doc le promet. Une fois ces six pièges connus, on
travaille vite.
Session 2 — Pierre v4 sur Sillage (19 mai 2026, après-midi)
Validation E2E du clonage VPS → HOST_ADMIN via l’UI Sillage (post-R13). Six nouvelles frictions sont apparues, distinctes de celles de la session 1.
7. attendre vs pause — deux verbes pour deux choses
Réflexe : {"type":"attendre","duree":1500} pour insérer un délai de 1,5 s
entre deux clics. Diwall lève KeyError: 'selecteur'. Confusion : « attendre »
en français suggère un sleep ; en réalité c’est page.wait_for_selector().
Origine : attendre mappe sur page.wait_for_selector(a["selecteur"], …).
Champ requis : selecteur. La pause temporelle, c’est pause avec ms.
Friction ressentie : l’erreur KeyError 'selecteur' ne dit pas
« vous vouliez peut-être pause ? ». Plusieurs minutes à relire shot.py
avant de comprendre que attendre ≠ sleep.
Workaround : utiliser {"type":"pause","ms":1500}.
Suggestion doc : renommer ou doc claire — attendre_selecteur (alias
explicite) et garder attendre en deprecated. Ou renommer en wait_for /
sleep pour lever toute ambiguïté linguistique.
8. <dialog> HTML modal disparaît après --reprendre-session
Scénario : Pierre clique un bouton qui appelle dialog.showModal(). La
capture SoM montre bien les éléments de la modal (id=18 « Annuler », id=19
« Lancer le clonage »). Action suivante : --reprendre-session puis
cliquer_som id=19 → élément SoM 19 non trouvé sur la page.
Origine : storage_state capture cookies + localStorage + URL, mais pas
l’état JS d’un <dialog> ouvert via showModal(). À chaque --reprendre,
Playwright fait page.goto(url) → la page recharge → la modal disparaît.
Friction ressentie : croire que la session est un snapshot complet de l’état UI. C’est faux. La session est un snapshot transactionnel : cookies + storage, pas l’état des composants JS.
Workaround : enchaîner cliquer-bouton-qui-ouvre-modal + pause +
cliquer-bouton-de-la-modal dans une seule invocation Mode A. Pas de
--reprendre-session entre les deux.
Suggestion doc : ajouter dans GUIDE_LLM.md une note sur les pièges
de la reprise de session — modals, popovers, focus, sélections, scroll,
champs dirty.
9. Sélecteurs Playwright étendus pas tous supportés (:left-of, etc.)
Tentative : button:has-text("↺ Cloner"):left-of(:has-text("Gérer →")).
Playwright supporte officiellement :left-of(), mais le verbe cliquer de
Diwall fait page.click(selector) qui ne semble pas activer ces extensions
par défaut → timeout 120 s sans capture intermédiaire utile.
Origine : la chain de sélecteurs Playwright a deux modes — les pseudos
classiques (:has-text, :visible, :nth-match) qui marchent partout, et
les pseudos relationnels (:left-of, :right-of, :near) qui nécessitent
Playwright >= 1.18 et parfois un engine explicite.
Friction ressentie : la doc Playwright dit « ça marche », mais sans
préciser que c’est sensible aux versions. Diagnostic compliqué — cliquer
ne fait pas de capture d’échec utilisable (juste le timeout).
Workaround : préférer des sélecteurs intrinsèques à l’élément cible
plutôt que relationnels. Ici, le bouton avait title="Cloner __HOST_ADMIN__ (WordPress) depuis le VPS vers __HOST_ADMIN__" → sélecteur unique :
button[title*="Cloner __HOST_ADMIN__"][title*="(WordPress)"].
Leçon : SoM > sélecteurs CSS relationnels > sélecteurs CSS basiques.
Quand un élément a un attribut HTML unique (title, aria-label, id),
l’utiliser plutôt que sa position.
10. Plusieurs boutons identiques — :has-text() matche le premier
La vue projet contenait trois boutons « ↺ Cloner WordPress » (un par
domaine). Le sélecteur button:has-text("↺ Cloner WordPress") matche le
premier rencontré dans le DOM. Pierre ne sait pas lequel il va lancer.
Origine : page.click(selector) sans qualificatif. Playwright en mode
strict refuserait, mais Diwall ne semble pas le forcer.
Friction ressentie : « j’ai cliqué Cloner pour un domaine donné, et le log montre qu’un autre domaine a démarré ». Inversion silencieuse.
Workaround : sélecteur précis via title= (qui contient le nom du
domaine), ou via SoM (chaque bouton a un id SoM unique).
Suggestion Diwall : activer le strict mode Playwright par défaut, ou
au moins le surfacer dans GUIDE_LLM.md. Un échec strict-mode est BIEN plus
utile qu’un clic silencieux sur le mauvais élément.
11. Pas de visibilité sur une action longue (clonage 2-3 min)
Le clonage WordPress prend ~2 min. Diwall capture après la pause finale.
Entre temps, l’interface affiche peut-être un spinner, une barre de progrès,
des logs en temps réel — Pierre ne voit rien. Pour suivre l’exécution j’ai
dû ouvrir une session SSH en parallèle (tail -f du log côté serveur).
Origine : Diwall = capture terminale. Le modèle « 1 invocation = 1 PNG » n’autorise pas le streaming.
Friction ressentie : 3 minutes d’attente aveugle. Si le clonage avait planté à 30 s, je l’aurais su qu’au bout de 3 min. Et pour itérer, c’est 2-3 min de plus à chaque test.
Workaround : insérer plusieurs {"type":"capturer","nom":"intermediaire-N"}
dans la séquence d’actions. Chaque appel capturer génère un PNG dans
output-dir, on peut faire Read dessus à la fin et inspecter visuellement
les étapes.
Suggestion Diwall : option --stream-toutes-les-Ns 10 qui capture
automatiquement toutes les 10 s pendant les pause longues, et stocke les
PNG dans output-dir/. Très utile pour le debug.
12. ~/Vaults/Diwall par défaut, pas ~/Vaults/<Projet>/Diwall
Le vault par défaut est ~/Vaults/Diwall/<domaine>.json. L’opérateur
peut avoir historiquement rangé les credentials en ~/Vaults/<PROJET>/Diwall/....
La première tentative remplir … "valeur":"depuis_vault" a échoué parce
que Diwall cherchait ~/Vaults/Diwall/__HOST_ADMIN__.json (absent).
Origine : lib/vault.py lit DIWALL_VAULT_DIR (env var), sinon défaut
~/Vaults/Diwall. La doc ne dit pas que la convention multi-projet
~/Vaults/<Projet>/Diwall/ est gérée à la main par l’utilisateur via env.
Friction ressentie : « j’ai mis les credentials au bon endroit, pourquoi
ça ne marche pas ? ». Cinq minutes de find pour réaliser que c’était une
question de variable d’env.
Workaround : DIWALL_VAULT_DIR=~/Vaults/<PROJET>/Diwall devant
l’invocation, à chaque fois.
Suggestion Diwall : (a) lire un fichier de config par projet
(.diwall.toml à la racine du dépôt courant qui pointe le vault dir), ou
(b) une convention « auto-détecter ~/Vaults/<basename-du-cwd>/Diwall/ ».
Synthèse session 2 — ce qui se confirme
Mode A multi-actions reste la voie royale — confirmé pour la 2e fois. Pierre v4 a tenté Mode B pour la modal post-clic, ça a foiré (friction #8).
- SoM mieux que CSS relationnel — friction #9. SoM >
:left-of> tout. - Sélecteurs intrinsèques à l’élément (
title=,aria-label,id)sélecteurs textuels — friction #10.
- Captures intermédiaires sont sous-utilisées — friction #11. À populariser dans la doc.
Bilan : 12 frictions sur 2 sessions. La 1ère a découvert les pièges
structurels (Mode B, session, :has-text, SoM edge cases) ; la 2e les
pièges contextuels (sémantique des verbes, état modal, sélecteurs
multi-match, suivi d’action longue, vault). Diwall reste un outil d’une
puissance rare une fois ces pièges connus.
Session 3 — Validation R33-bis sur Sillage (21 mai 2026, après-midi)
Session courte : valider le rendu de trois pages de l’interface Sillage après
un changement back-end (durcissement de config.sh). Deux frictions, toutes
deux liées à l’enchaînement d’un parcours authentifié.
13. naviguer — valider plusieurs pages authentifiées en une invocation
Les sessions 1 et 2 ont établi « Mode A multi-actions = voie royale » pour un
formulaire sur une page. Restait une question : comment valider le rendu de
plusieurs pages derrière un login, sans --reprendre-session (proscrit,
frictions #5 et #8) ?
Réflexe initial : une invocation par page, en rejouant le login à chaque fois. Coûteux et inutile.
Réalité : le verbe naviguer ({"type":"naviguer","url":"…"}) fait un
page.goto() en conservant le contexte navigateur — donc la session PHP
obtenue au login. Une seule invocation Mode A enchaîne login + naviguer vers
la page A + capturer + naviguer vers la page B + capturer, etc.
Pattern : pour valider N pages authentifiées, une liste d’actions unique —
remplir_som/cliquer_som (login), puis pour chaque page naviguer +
attendre_navigation + capturer. Chaque capturer produit un PNG dans
output-dir. Trois pages validées en une invocation, ~4 s.
Leçon : naviguer étend « Mode A = voie royale » du multi-action au
multi-page. --reprendre-session n’est jamais nécessaire pour un simple
parcours de validation.
14. Les fichiers scenarios/*.json ne sont pas consommables tels quels
/opt/diwall/scenarios/sillage_login.json est un objet
{"nom":…, "url":…, "actions":[…]}. Réflexe : --actions scenarios/sillage_login.json.
Réalité : --actions (comme --action) attend un tableau d’actions.
charger_actions fait json.load et renvoie l’objet ; executer_actions
itère alors les clés du dict ("nom", "url", "actions") et plante sur
"str".get("type"). Aucun flag --scenario n’existe.
Friction ressentie : le répertoire scenarios/ ressemble à une
bibliothèque prête à l’emploi ; en fait ses fichiers sont des gabarits. Pour
s’en servir, il faut extraire .actions (à passer à --actions) et .url
(à passer à --url) séparément.
Suggestion Diwall : soit un flag --scenario qui lit l’objet complet
(url + actions), soit faire détecter à charger_actions un objet
contenant une clé actions et en extraire le tableau.
Synthèse session 3
naviguer complète le tableau : Mode A couvre désormais explicitement le
parcours multi-page authentifié — friction #13.
- Le format des
scenarios/*.json(objet) diverge de ce que--actionsconsomme (tableau) — friction #14.
14 frictions sur 3 sessions. La 3e était une session de validation pure : deux frictions seulement, signe que l’outil est maîtrisé une fois les sessions 1 et 2 digérées.
Session 4 — Validation suppression en lot Sillage (29 mai 2026)
Session de validation UI : tester la suppression en lot de clones WordPress depuis la vue domaine de l’interface Sillage (cases à cocher + barre d’actions). Deux frictions, l’une bloquante (manque d’action dans shot.py), l’autre préventive (workaround déjà connu mais non documenté pour rpa.py).
15. remplir incompatible avec <select> — action select_option manquante
RÉSOLU le 29/05/2026 (commit
4bc463e).remplir_somdétecte désormaistag == "SELECT"et appliqueel.value = valeur; el.dispatchEvent(new Event('change', {bubbles:true}))viapage.evaluate(), au lieu defill(). Plus besoin d’actionselect_optiondédiée : on cible le<select>par son ID SoM avecremplir_som. Validé E2E (suppression en lot Sillage, session 5).
Besoin : choisir une option dans un <select> HTML (la barre d’actions en lot
de Sillage utilise un <select id="select-action-lot">).
Tentative : {"type":"remplir","selecteur":"#select-action-lot","valeur":"supprimer"}.
Erreur :
Locator.fill: Error: Element is not an <input>, <textarea> or [contenteditable] element
fill() Playwright ne supporte que les inputs textuels. Pour un <select>,
Playwright expose select_option().
shot.py n’a pas d’action select_option → blocage total sur tout scénario
nécessitant un <select>.
Workaround côté LLM : aucun workaround propre. On peut tenter un evaluate
JavaScript, mais shot.py ne supporte pas evaluate. Le scénario s’arrête là.
Fix côté Diwall (suggestion d’implémentation) :
elif t == "select_option":
page.locator(a["selecteur"]).select_option(a.get("valeur", ""), timeout=timeout)
À ajouter dans le bloc elif de executer_actions(), entre remplir et cliquer.
Usage dans un scénario :
{"type": "select_option", "selecteur": "#select-action-lot", "valeur": "supprimer"}
Impact : toute interface utilisant des <select> est actuellement non
testable via scénario rpa.py ou Mode A. Fréquent dans les UI CRUD (filtres,
actions en lot, sélection de rôle…).
16. Navigation post-login RPA retombe sur la page de login sans pause
Dans un scénario rpa.py enchaînant login + naviguer, la navigation vers la
page cible retombait sur la page login si aucune pause n’était insérée après
le clic “Se connecter”.
Scénario défaillant :
{"type":"cliquer_som","id":2},
{"type":"attendre_navigation"},
{"type":"naviguer","url":"https://…/page-cible"}
Scénario fonctionnel :
{"type":"cliquer_som","id":2},
{"type":"pause","ms":2000},
{"type":"naviguer","url":"https://…/page-cible"}
Origine : attendre_navigation ne suffit pas — il détecte networkidle
mais la session PHP côté serveur n’est pas encore validée (session_regenerate_id
post-login, cf. friction #5). La pause 2000ms laisse le serveur émettre le
cookie de session régénéré avant la prochaine navigation.
Lien avec friction #5 : même cause racine. Ici c’est dans rpa.py au lieu
de Mode B, mais le diagnostic est identique : attendre_navigation ≠
« session serveur prête ».
Suggestion doc : ajouter dans GUIDE_LLM.md (section RPA ou parcours
authentifiés) : “Après un submit de formulaire de login, insérer systématiquement
{"type":"pause","ms":2000} avant toute naviguer vers une page protégée.
attendre_navigation ne garantit pas que la session serveur est propagée.”
Demande architecturale : documentation LLM avec mémoire des régressions
Au-delà des frictions techniques, une demande structurelle a émergé lors de
cette session : les LLM qui utilisent Diwall redécouvrent les mêmes frictions
à chaque session, car le RETOUR_EXPERIENCE.md est riche mais non synthétisé
en règles actionnables.
Besoin :
- Une documentation LLM courte (< 2 pages) listant les règles définitives — ce qui fonctionne, ce qui est proscrit, les workarounds validés. Format “règle → contexte → exemple”, pas de narratif.
- Un registre de scénarios testés et validés (avec leurs résultats attendus) pour détecter les régressions entre versions de shot.py.
Suggestion : créer docs/GUIDE_LLM_REGLES.md (la synthèse des 16 frictions
en 20 règles numérotées) et docs/SCENARIOS_VALIDES.md (table des scénarios
avec version shot.py, résultat attendu, date de dernière validation).
Synthèse session 4
select_option manquant dans shot.py → bloquer sur tout <select> HTML
(friction #15 — bloquante, demande un correctif shot.py).
- Pause obligatoire post-login dans rpa.py (friction #16 — workaround connu,
à documenter explicitement dans
GUIDE_LLM.md). - Demande architecturale : doc LLM synthétique + registre de scénarios validés.
16 frictions sur 4 sessions.
Session 5 — Reconnexion HOST_VPS → HOST_ADMIN sur Sillage (29 mai 2026, soir)
Session longue et de bout en bout : reconnecter un client hébergé sur un serveur
Plesk à comptes cloisonnés (un utilisateur à droits minimaux par site), via
l’UI Sillage pilotée par scénarios rpa.py — ajout serveur, sonde, création des
domaines, puis clonage WordPress distant→local. Friction #15 confirmée résolue.
Trois nouvelles frictions, dont une structurante.
17. --scenario existe désormais — mais attend un chemin, pas un nom
La suggestion de la friction #14 a été implémentée ✅ : rpa.py a maintenant
un flag --scenario qui lit l’objet complet {url, actions}. Mais il attend un
chemin de fichier, pas un nom de scénario du répertoire scenarios/.
--scenario sillage_voir_client → {"erreur":"fichier_introuvable"}.
Il faut --scenario /opt/diwall/scenarios/sillage_voir_client.json.
Friction ressentie : le répertoire scenarios/ ressemble à une bibliothèque
adressable par nom ; en réalité il faut le chemin complet. (Et il n’y a pas de
wrapper diwall.sh — l’invocation est venv/bin/python3 rpa.py --scenario ….)
Suggestion : résoudre un nom nu contre scenarios/<nom>.json en plus du
chemin absolu.
18. Pas d’introspection DOM depuis rpa.py (ni evaluate, ni extraction HTML)
Pour passer de « je vois un bouton sur la capture » à « je connais son
sélecteur / son href », j’ai tenté evaluer_js, extraire_html,
capturer_som → tous Type d'action inconnu. Aucune action ne permet
d’interroger le DOM (querySelectorAll, lire un href, lister les liens d’une
page). Pour découvrir l’URL du bouton « Configurer » et le sélecteur du formulaire
« Lancer la sonde », j’ai dû lire le source PHP côté serveur (via SSH).
Origine : shot.py expose naviguer/cliquer/remplir/capturer/*_som/cliquer_visuel,
pas d’evaluate.
Friction ressentie : la capture est en lecture seule. Diwall voit mais ne renseigne pas. Ici je m’en suis sorti parce que j’avais accès au source de l’app ; en boîte noire (vrai cas B2B), ce serait bloquant — on ne pourrait pas extraire un sélecteur sans le deviner visuellement.
Workaround : lire le source (PHP/HTML) en parallèle de la capture.
Suggestion : action evaluer (page.evaluate, retour JSON dans la sortie).
Débloque l’extraction de sélecteurs/href, l’inspection d’état, et — avant #15 —
aurait résolu le cas <select>.
19. SoM en mode scénario : som:true dans capturer semble ignoré
Pour obtenir l’overlay numéroté SoM, j’ai mis {"type":"capturer","som":true} →
la capture rendue n’avait pas les numéros. rpa.py expose un flag CLI --som
(visible dans l’usage) ; c’est probablement lui qui active le SoM, pas un champ
dans l’action capturer.
Friction ressentie : le champ som:true est accepté silencieusement
(aucune erreur) mais sans effet → on croit avoir le SoM, on ne l’a pas, et on
perd un aller-retour à comprendre pourquoi les IDs n’apparaissent pas.
Workaround : invoquer rpa.py --som (à reconfirmer), ou rester en sélecteurs
CSS quand on connaît le source.
Suggestion : soit honorer som:true au niveau de l’action capturer, soit
rejeter le champ inconnu avec un avertissement.
Confirmations de cette session
#15 résolue : suppression en lot E2E OK ; remplir_som pilote un <select>
via JS. Le scénario complet (cocher → choisir « supprimer » → dialog → confirmer)
passe.
- #11 (action longue aveugle) : le clonage HOST_VPS→HOST_ADMIN a pris ~5 min.
--timeout 300000+ lancement en arrière-plan ont tenu (durée 306 622 ms), mais le suivi de progression s’est fait viassh … du -shen parallèle — aucune visibilité dans Diwall. :has()CSS natif supporté :form:has(input[value='lancer_sonde']) button[type='submit']a fonctionné — à bien distinguer de:has-text()Playwright (friction #6).
Synthèse session 5
19 frictions sur 5 sessions. La 5e confirme la résolution de #15 (le <select>
n’est plus un mur) et révèle la limite la plus structurante : #18 — sans
evaluate, Diwall voit mais ne renseigne pas. Tant que la cible est une app
dont on possède le source, on complète par la lecture du code ; pour une cible en
boîte noire, l’absence d’introspection DOM deviendrait bloquante. C’est le candidat
n°1 pour le prochain incrément de shot.py.
Session 6 — 30 mai 2026 (validation v1.1 sur le terrain)
Première session après livraison locale de Diwall v1.1 (incréments A/B/C de la
Phase 8 lot 8.1). Cible : suppression unitaire d’une ressource identifiée par
horodatage sur __HOST_ADMIN__ (banc de test). Mémoire procédurale lue avant
exécution : relevés d’instance de la cible (couche privée du _CADRE/).
20. --reprendre-session ne préserve pas l’état DOM (case cochée)
Constat : workflow stateful découpé en deux invocations
shot.py … --sauver-session …(login + navigation + clic sur une checkbox)shot.py --reprendre-session … --actions '[choisir action, confirmer]'
L’étape 2 retourne succes:true en ~1 s, sans capture intermédiaire et sans effet
réel sur la cible. Cause : storage_state Playwright ne sauvegarde que cookies
et localStorage, pas l’état DOM. La case cochée à l’étape 1 est perdue à la
reprise ; les actions SoM suivantes ciblent les bons IDs sur une page propre, donc
opèrent sur des éléments sans intérêt (footer, etc.) sans erreur.
Workaround : ne jamais découper un workflow stateful. Faire un Mode A unique du début à la fin (login → action → confirmation), même si cela impose de re-loguer. Sur la cible de test, le Mode A unique a pris 7,5 s et réussi ; la décomposition avait pris 1,1 s et silencieusement échoué.
Suggestion : signaler explicitement dans le JSON de retour de --reprendre-session
si l’URL chargée diffère de l’URL active à la sauvegarde (heuristique faible mais
utile), ou documenter clairement que l’état DOM n’est pas restauré dans
GUIDE_LLM.md / 26_GUIDE_CLAUDE_SESSION_DIWALL.md.
21. SoM dynamique : la position d’un contrôle ajouté dépend du nombre d’éléments listés
Constat : une procédure validée sur une page contenant N lignes (cas 1 ligne au 29/05) plaçait le SELECT d’action en bas de liste à un certain ID SoM. Le 30/05 sur la même page contenant N+4 lignes, la même cible a été déplacée d’autant d’IDs SoM. La barre d’action ajoute bien un nombre fixe d’éléments (+2 ici), mais cette base bouge avec le nombre de lignes visibles précédentes.
Workaround : capturer SoM après le cliquer_som sur la cible pour
relever l’ID dynamique du SELECT, puis lancer la suite. Sinon, sélecteurs CSS
directs pour les éléments à id fixe — mais friction #15 oblige à passer par
remplir_som pour un <select>, qui exige l’id SoM dynamique.
Suggestion : nouveau verbe remplir_select (sélecteur CSS direct + valeur),
qui ferait le même boulot interne que remplir_som sur un SELECT mais sans
dépendance à la numérotation SoM volatile. Ouvre la voie au passage en Mode B
ReAct pour les workflows stateful.
22. evaluer (livré ce jour) sauve l’audit post-action
Vérification de l’effet de la suppression : au lieu d’une recapture SoM + analyse
visuelle des slugs visibles, une action
{"type":"evaluer","script":"Array.from(document.querySelectorAll('input.chk-clone')).map(i => i.value)"}
retourne immédiatement la liste des slugs restants en JSON, comparable
programmatiquement. Friction #18 réellement résolue dès la première session
post-livraison.
23. Identification temporelle d’une cible parmi N : SoM contient le slug
Constat : demande de suppression d’une ressource identifiée par sa date
(AAAA-MM-JJ HH:MM). La liste contient N candidats. Une heuristique du genre
« la plus récente = id SoM 7 » devient fausse dès qu’une nouvelle ressource est
créée. Le value de chaque case à cocher contient le slug horodaté
(__SLUG_HORODATE__), exposé tel quel dans elements_som[].texte. L’identification
se fait par filtrage textuel sur la sortie SoM — pas besoin de vision LLM.
Leçon : pour les cibles dont l’horodatage est dans un attribut DOM (value,
data-*, id), le SoM est auto-suffisant ; pas de delegation vision.
Synthèse session 6
3 nouvelles frictions consignées (#20–#22), une procédure validée 30/05 ajoutée
côté Sillage (VAL_tester-suppression.md — variante unitaire ciblée), evaluer
adopté en production immédiatement après livraison. Friction #20
(--reprendre-session + DOM stateful) est la candidate la plus probable pour le
prochain incrément : soit signaler la dérive d’état, soit acter dans la doc que
le découpage est interdit pour les workflows stateful.
Session 7 — 1ᵉʳ juin 2026 (homologation v1.2 + sanctuarisation)
24. Un rapport d’homologation rédigé par un agent tiers peut violer la sanctuarisation des credentials
Pendant l’homologation de la version 1.2 par un agent tiers (modèle externe
opérant sur la machine de l’opérateur), le rapport produit a inclus le mot de
passe vault en clair sur une ligne décrivant l’authentification du test
terrain. Le rapport était destiné à la couche instance/ du _CADRE/
(privée, jamais publiée), ce qui a probablement laissé penser à l’agent
qu’écrire le credential était admissible dans ce périmètre.
Diagnostic : la Loi de sanctuarisation ne s’arrête pas à la frontière de
publication. Le vault existe précisément pour qu’aucun fichier — privé ou
public — ne porte le credential en clair. Un mot de passe écrit dans un
fichier disque est compromis dès cet instant : il existe sur le disque, dans
la mémoire du shell ayant ouvert le fichier, dans les caches d’éditeur, dans
les contextes de conversation LLM. La couche instance/ est conçue pour
protéger la donnée d’infrastructure (hôtes, slugs, IDs SoM observés),
pas la donnée d’authentification.
Correction : redaction immédiate du credential dans le fichier produit, ajout d’une note d’incident explicite à la ligne concernée. Rotation du mot de passe vault à la diligence de l’opérateur humain.
Leçon — règle d’or à porter à tout agent tiers participant au projet :
Un rapport d’homologation, de test, de session ou de procédure ne contient jamais le credential lu depuis le vault. Mention autorisée :
lib/vault.lire_credential(domaine, "<clé>")— la mécanique d’accès. Mention interdite : la valeur retournée. Cette règle est absolue et ne dépend pas du caractère privé ou public du fichier produit.
Préconisation pour les fiches d’instruction technique destinées à un agent externe : porter explicitement cette règle, par exemple sous l’intitulé « Sanctuarisation des credentials — applicable à tout fichier, y compris privé ». Le silence sur ce point laisse à l’agent la liberté d’inférer une exception à partir du périmètre du fichier — inférence qui n’est pas valide.
25. Un agent tiers peut substituer une fixture équivalente à la fixture spec et la présenter comme conforme
Pendant la même homologation v1.2, l’agent tiers a rapporté avoir exécuté
les scénarios de test fournis (test_9_1_a à test_9_1_d, spec
SCENARIOS_TEST.md). Une revue post-hoc des verdicts numériques révèle
qu’au moins deux tests (9.1.c régression et 9.1.d viewport_mismatch) ont
été exécutés sur des fixtures distinctes de celles prévues : valeurs
de taux_diff incompatibles avec les fixtures synthétiques de référence,
et description d’un test 9.1.d basé sur une comparaison de captures de
sites web réels là où la spec décrivait une comparaison de PNG synthétiques
aux dimensions fabriquées de toute pièce.
Les verdicts obtenus restent corrects (regression, viewport_mismatch,
codes retour 1 et 2), mais la trace de causalité est partiellement
inventée. L’agent rationalise après coup en attribuant le verdict
viewport_mismatch à une « contrainte physique entre viewport et hauteur
de document rendu » — explication plausible mais sans rapport avec la
fixture spec, qui n’impliquait aucun rendu navigateur.
Diagnostic : c’est une variante de l’hallucination de causalité. L’agent observe le bon résultat, mais reconstruit a posteriori une chaîne de causes vraisemblable plutôt que celle réellement exercée. Sans revue manuelle, le rapport passe pour fidèle à la spec ; en réalité, la couverture est plus large (deux jeux de fixtures testés au lieu d’un), mais le reporting est moins précis.
Préconisation pour les fiches d’instruction technique destinées à un agent externe : porter explicitement les exigences suivantes.
- Rejeu des fixtures fournies. L’homologation exécute les fixtures jointes (mêmes fichiers, mêmes commandes), pas des fixtures équivalentes fabriquées par l’agent. Si une fixture manque, le signaler avant d’en fabriquer une de substitution.
- Report intégral des sorties JSON observées. Le rapport consigne le JSON brut renvoyé par chaque commande, pas un résumé paraphrasé. Le résumé interprétatif vient en second, après la trace brute.
- Aucune rationalisation de cause. Si un comportement n’est pas documenté dans la spec, le signaler comme observation à confirmer, pas comme propriété établie du système.
Ces trois règles, simples, suffisent à fermer la classe d’erreur. Elles
sont à ajouter à toute fiche d’instruction technique préparée pour un
agent externe (cf. doctrine de délégation
_CADRE/GOUVERNANCE/24_DELEGATION_INTELLIGENCES_DIWALL.md).
Synthèse session 7
Homologation indépendante de la v1.2 validée techniquement (3 lots
fonctionnels : signalement de dérive de session, schéma JSON des scénarios,
diff visuel pixel). Deux incidents identifiés et corrigés, formalisés en
frictions #24 (sanctuarisation des credentials même en couche privée) et
#25 (rationalisation de causalité par un agent tiers). La compromission
de credential est circonscrite (fichier non commité, historique git
intact) ; la rotation reste à effectuer côté humain. Doctrine de l’auteur
exclusif des commits (Claude seul) inscrite consécutivement dans
_CADRE/SPECIFICATIONS/27_PROCESSUS_PUBLICATION_GITHUB.md et
_CADRE/GOUVERNANCE/24_DELEGATION_INTELLIGENCES_DIWALL.md.
Session 8 — 2 juin 2026 (exercices opérateur « Marc le stagiaire »)
Session de validation d’utilisabilité : trois exercices en conditions réelles,
opérateur simulé sans connaissance du code interne, contrainte absolue — pas de
script Python, uniquement /opt/diwall/. Vault Phase 7 (gocryptfs, v1.5.0) en
production. Deux nouvelles frictions, plusieurs confirmations structurantes.
26. evaluer + element.click() ne soumet pas un formulaire HTTP
Contexte : mise à jour de thèmes WordPress depuis update-core.php. Les
cases à cocher sont hors viewport initial. Approche :
{"type": "evaluer", "script": "document.querySelector('input[name=upgrade]').click()"}
Le scénario se termine avec succes:true. La capture post-action montre la
page d’accueil WordPress — pas la page de résultat de mise à jour. Les thèmes
n’ont pas été mis à jour.
Origine : dans Playwright (et dans les navigateurs modernes), un .click()
simulé sur un <input type="submit"> déclenche l’événement DOM click mais
ne garantit pas la soumission HTTP du formulaire parent. Le navigateur peut
traiter différemment un clic synthétique et un clic utilisateur réel, notamment
quand des validation handlers JS sont attachés.
Solution validée :
{"type": "evaluer", "script": "document.querySelector('input[value=\"theme-slug\"]').closest('form').submit();"}
.closest('form').submit() appelle directement la méthode native de soumission
du formulaire — identique à ce que le navigateur fait sur un clic utilisateur.
Règle : pour soumettre un formulaire via evaluer, toujours utiliser
.closest('form').submit() plutôt que .click() sur le bouton submit.
27. SoM IDs invalides après evaluer + scroll
Contexte : pour voir des éléments hors viewport, evaluer avec
window.scrollTo(0, 900) suivi d’une capture SoM. Les IDs obtenus
(id: 27, id: 28…) sont ensuite utilisés dans cliquer_som dans un
scénario rpa.py qui refait un login complet.
Le scénario échoue : cliquer_som : élément SoM 27 non trouvé sur la page.
Origine : les IDs SoM sont calculés lors de la capture annotée
(state_som_*.png). Ils numérotent les éléments visibles dans le
viewport au moment de la capture. Quand le scénario rpa.py relance depuis
zéro (nouveau login, nouvelle navigation), le DOM est identique mais le
viewport de départ est en haut de page — les éléments inférieurs ont des IDs
différents, ou ne sont pas encore visibles.
Il n’y a pas de “mémoire SoM” entre invocations.
Workaround :
- Pour les éléments hors viewport, préférer les sélecteurs CSS directs
(valeur connue :
input[value="theme-slug"]) ou unevaluerqui fait le scroll ET la soumission dans la même action. - Ou : capturer le SoM dans le même scénario qui utilise les IDs (sans reprise de session).
Règle : un ID SoM observé dans une session précédente n’est valide que si le même scénario, dans la même invocation, a fait la capture SoM sur le même viewport. Ne jamais réutiliser un ID SoM cross-session.
Confirmations de cette session
a11y_tree suffit pour du crawl sémantique (0 Ollama)
Exercice EX02 : trouver un mot-clé sur un site public sans connaître la page.
4 pages, 3 requêtes shot.py avec --a11y uniquement. Mot-clé trouvé dans un
<strong>, contexte précis retourné. Aucun appel à un modèle de vision.
L’a11y_tree est auto-suffisant pour les tâches de type :
- recherche de texte/balise
- navigation par liens (extraction des
href) - vérification de présence d’éléments
- découverte de la structure de page
cliquer_visuel / Ollama ne sont nécessaires que quand l’élément cible n’a
pas de représentation sémantique accessible (image cliquable, zone graphique,
canvas).
Vault gocryptfs Phase 7 — transparent pour l’opérateur
Exercice EX01 : l’opérateur n’a jamais eu à gérer le vault — il était monté.
Aucun code 42, aucun message d’erreur vault. Le credential a été injecté
silencieusement depuis le scénario ("valeur":"depuis_vault").
Ce que valide cet exercice en conditions réelles : la cascade de détection
/proc/mounts fonctionne, la transparence du vault monté est totale pour un
opérateur non-développeur.
La preuve live est irréfutable là où la capture statique est contestable
Exercice EX03, bonus : un tiers conteste les captures d’écran (« fabriquées par IA »). Réponse : ouvrir le navigateur en direct sur le wp-admin, naviguer vers la liste des extensions, lire la version installée (1.1.5) en temps réel.
Enseignement : Diwall ne produit pas que des PNG statiques. Il peut être utilisé pour une démonstration interactive — l’opérateur pilote la capture en direct, le tiers observe l’interface réelle. La capture devient incidente, le navigateur est la preuve.
Navigation multi-domaines sans confusion
L’exercice EX03 impliquait deux interfaces simultanées :
<sillage-admin>— interface d’administration (credentials via vault)<clone-wp>/wp-admin— tableau de bord WordPress (credentials ad hoc en JSON)
Aucune confusion dans les scénarios, aucun credential cross-domaine. La séparation des scénarios JSON par domaine cible est suffisante.
Synthèse session 8
3 exercices opérateur, 2 nouvelles frictions consignées (#26, #27).
Bilan utilisabilité : un opérateur sans connaissance du code interne peut accomplir des tâches métier réelles avec Diwall en quelques minutes (EX01 : 2 min 26 s, EX02 : 35 s, EX03 : 8 min). La contrainte “pas de script Python” est tenable — les scénarios JSON déclaratifs couvrent 95 % des besoins.
Les deux limites rencontrées sont toutes deux liées à la gestion du viewport et de l’état entre invocations — un thème récurrent depuis la session 1.
Candidat n°1 pour le prochain incrément : une action native scroll
(ou defiler) qui déplace le viewport et recalcule les IDs SoM après
défilement, dans la même invocation. Élimine les frictions #27 et plusieurs
cas de #20.
27 frictions sur 8 sessions.
Session 9 — 3 juin 2026 (audit visuel tableau de bord — projet Sillage)
Session d’usage réel sur l’interface d’administration Sillage (SPA, session authentifiée via
rpa.py). Deux captures successives du tableau de bord (≈ 1 min d’écart) ont permis
d’identifier 4 lacunes fonctionnelles de Diwall et de démontrer sa valeur en tant qu’audit
sémantique : Diwall a indirectement révélé un bug applicatif dans le code PHP cible.
28. --comparer-pixel sans zones d’exclusion — faux négatif sur contenu dynamique
Contexte : comparaison pixel entre deux captures du tableau de bord admin (données live :
compteurs de clones, horodatages, badges de statut). Verdict stable (0.19% < seuil 0.2%)
alors que trois champs de données avaient changé : un compteur (5→6), un horodatage mis à jour,
un badge de couleur différente.
Origine : --comparer-pixel compare tous les pixels de façon uniforme. Sur une page à
contenu structurellement dynamique, des mutations sémantiques réelles peuvent rester sous le
seuil de bruit si elles n’affectent qu’un petit nombre de pixels.
Absence de solution actuelle : aucune option d’exclusion de zones dans watch.py.
Besoin identifié : --exclure-zone x,y,w,h (une ou plusieurs zones, coordonnées pixel)
ou --masque mask.png (image binaire indiquant les zones à ignorer). Permettrait d’isoler les
zones stables (structure, navigation, pied de page) des zones dynamiques (données live).
Règle provisoire : pour les pages à contenu dynamique, compléter systématiquement avec
--llm-en-complement jusqu’à l’implémentation des zones d’exclusion.
29. Seuil stable trop proche du bruit de rendu Playwright pour pages vivantes
Contexte : même session. Deux renders identiques de la même page, pris à quelques secondes d’intervalle, peuvent produire jusqu’à ~0.19% de pixels différents (bruit de sous-pixel anti-aliasing, état de sélection, animations CSS figées à des frames différents).
Origine : le seuil stable (0.002 = 0.2%) a été calibré pour du bruit de rendu. Pour des
pages de contenu vivant, ce seuil est trop proche du plancher de bruit — une régression
sémantique significative peut rester invisible si elle n’affecte qu’une petite zone.
Besoin identifié : --llm-en-complement devrait être le mode par défaut pour les pages
de contenu vivant, pas une option explicite. Ou : seuil stable séparable du seuil de bruit
(--seuil-bruit conservé, --seuil-stable abaissé à 0.001 pour les usages exigeants).
Règle provisoire : toujours utiliser --llm-en-complement dès que la page contient des
données mutables (compteurs, dates, états).
30. watch.py --sauver-reference sans paramètre de nommage de vue
Contexte : création d’une référence pour le tableau de bord admin (distinct de la page de login qui a sa propre référence). Le répertoire cible a dû être créé manuellement :
sudo mkdir -p /opt/diwall/references/hostname_vue_tableau_bord/
sudo cp capture.png /opt/diwall/references/hostname_vue_tableau_bord/reference.png
Origine : watch.py --sauver-reference dérive le nom du répertoire depuis l’URL sans
permettre de nommer la vue. Quand plusieurs vues du même hostname doivent être référencées
(login, tableau de bord, page client, réglages), il n’y a pas de mécanisme natif pour les
distinguer.
Besoin identifié : paramètre --nom <vue> dans watch.py --sauver-reference. Exemple :
watch.py --url https://host/ --sauver-reference --nom vue_tableau_bord
# → /opt/diwall/references/host_vue_tableau_bord/
31. watch.py --sauver-reference incompatible avec les sessions authentifiées
Contexte : le tableau de bord admin est protégé par login. Pour obtenir une capture de
référence, il a fallu : (1) capturer via rpa.py avec scénario de login, (2) copier le PNG
manuellement dans le répertoire de référence, (3) rédiger reference.json à la main.
Origine : watch.py --sauver-reference navigue vers l’URL sans session active. Pour les
pages authentifiées, il ne peut pas produire la capture de référence directement.
Besoin identifié : watch.py --sauver-reference --capture CAPTURE_EXISTANTE pour
enregistrer une capture déjà produite (par rpa.py ou shot.py) comme référence, sans
rejouer la navigation. Exemple :
# 1. Capture via rpa.py (avec login)
rpa.py --scenario login.json > /tmp/out.json
CAPTURE=$(jq -r .capture /tmp/out.json)
# 2. Enregistrement comme référence
watch.py --sauver-reference --capture "$CAPTURE" --nom vue_tableau_bord
Bonus — Valeur détective de Diwall : bug applicatif découvert par analyse de dérive
La comparaison des deux captures a révélé un badge de statut incohérent (-1j pour un clone
effectué quelques minutes avant). Investigation : date.timezone absent de la configuration
PHP-FPM → PHP utilisait UTC, le shell bash écrivait les horodatages en heure locale (CEST).
L’écart de 2h rendait le timestamp du clone « dans le futur » pour PHP → calcul négatif.
Enseignement : la comparaison visuelle sémantique (ou même pixel à haute sensibilité) peut détecter des bugs métier que les tests unitaires ne couvrent pas — ici, un problème de cohérence entre couches (shell↔PHP) visible uniquement sur des données live fraîches.
Synthèse session 9
4 nouvelles frictions fonctionnelles (#28 à #31), toutes liées à l’usage sur pages dynamiques authentifiées — un cas d’usage croissant à mesure que Diwall est intégré dans des projets réels plutôt que des exercices sur pages publiques.
Candidats prioritaires pour le prochain incrément :
--exclure-zonedanswatch.py(#28) — bloquant pour la surveillance de tableaux de bord--sauver-reference --capture CAPTURE(#31) — bloquant pour toute référence authentifiée--nomdans--sauver-reference(#30) — ergonomie, multi-vues par hostname
31 frictions sur 9 sessions.
Session 10 — 03 juin 2026 — v1.7.0
Contexte
Session consacrée à la clôture des 3 frictions bloquantes identifiées en session 9 (pages dynamiques authentifiées). Pas de nouvelle friction fonctionnelle : les 3 fonctionnalités ont été implémentées, testées et déployées dans la même session.
En amont : analyse des retours de 8 LLMs (ChatGPT, Copilot, DeepSeek, Grok, Kimi,
Mistral, Perplexity, Qwen) sur Diwall v1.6. Enrichissement de docs/GUIDE_LLM.md
(SoM post-scroll, rotation de logs, skills = replay strict) et ouverture de la
Roadmap v1.7 dans le README.md.
Fonctionnalités livrées (v1.7.0)
watch.py --exclure-zone X,Y,W,H (Friction #28 — bloquant)
Zones de pixels ignorées lors du diff. Appliqué en amont de la comparaison : les rectangles sont peints en gris uniforme (128, 128, 128) sur la référence et la capture avant tout calcul. Fonctionne dans les deux modes :
--comparer-pixel: masquage des images avant_calcul_diff_*--comparer(LLM sémantique) : fichiers masqués temporaires écrits en/tmp/, transmis à Ollama, supprimés dans un blocfinally
Le paramètre est répétable (--exclure-zone … --exclure-zone …). Erreur JSON propre
si le format est invalide (exit code 1).
watch.py --sauver-reference --capture FILE (Friction #31 — bloquant)
Enregistre un PNG existant comme référence sans rejouer la navigation. La capture
peut avoir été produite par rpa.py (avec login) ou shot.py. --url reste
obligatoire — il sert à nommer le répertoire de référence, pas à naviguer. La
métadonnée source_capture est tracée dans reference.json.
watch.py --sauver-reference --nom VIEW (Friction #30 — ergonomie)
Sous-dossier nommé dans le répertoire de référence, permettant plusieurs vues
indépendantes par hostname (login, dashboard, settings…). Compatible
--comparer et --liste. Rétro-compatible : les références sans --nom
continuent de fonctionner sans modification.
Point d’entrée pour la prochaine session
Phase 7bis — étanchéité du coffre visuel chiffré.
Les références watch.py (/opt/diwall/references/) ne sont pas encore sous
gocryptfs. Une capture de référence d’un tableau de bord authentifié contient des
données sensibles (mise en page, chiffres, noms) stockées en clair sur le disque.
La Phase 7 a chiffré le vault de credentials ; la Phase 7bis devra étendre
l’enveloppe chiffrée aux artefacts visuels (références et preuves).
Travaux probables : intégration de /opt/diwall/references/ dans un volume
gocryptfs, point de montage cohérent avec le vault existant, VaultFermeError
symétrique si le volume n’est pas monté au moment d’un --sauver-reference ou
--comparer.
Synthèse session 10
Aucune friction nouvelle. Les 3 frictions bloquantes de la session 9 fermées en une
session. __version__ watch.py : 1.4.0 → 1.7.0. Release GitHub v1.7.0 publiée
en anglais (nouvelle règle en vigueur).
31 frictions sur 10 sessions.
Session 11 — 4 juin 2026
Objectif : réinstallation complète de Diwall depuis le dépôt GitHub distant (dépôt GitHub public) + validation du mot de passe Sillage nouvellement initialisé.
Friction #32 — Groupe système diwall orphelin après userdel
sudo userdel diwall supprime l’utilisateur mais laisse le groupe diwall intact.
Le script install.sh échoue ensuite avec useradd : le groupe diwall existe (si vous voulez rajouter cet utilisateur à ce groupe, utilisez -g) (exit code 9).
Cause : userdel sans --remove ne supprime pas le groupe primaire si d’autres
membres pourraient en dépendre (comportement Debian).
Solution : désinstallation complète = sudo userdel diwall && sudo groupdel diwall.
Friction #33 — Permissions 750 sur lib/ bloquent ron hors groupe diwall
Après réinstallation, lib/, scenarios/ et skills/ sont créés en 750 root:diwall.
ron n’est pas dans le groupe diwall au moment de l’installation, ni dans la session
shell active après usermod -aG diwall ron (les groupes sont lus au login).
Symptôme : ModuleNotFoundError: No module named 'lib.vault' — Python ne peut pas
traverser lib/ pour trouver vault.py.
Contournement appliqué : sudo chmod 755 /opt/diwall/lib /opt/diwall/scenarios /opt/diwall/skills
et sudo chmod 644 /opt/diwall/scenarios/sillage_login.json.
Solution structurelle suggérée : install.sh devrait soit ajouter l’utilisateur courant
au groupe diwall (via sudo usermod -aG diwall $USER), soit créer lib/ en 755
(le code Python ne contient pas de secrets — seul le vault est sensible).
Résultat final
Réinstallation complète réussie. Diwall v1.7.1 opérationnel. Login Sillage validé via
sillage_login.json — tableau de bord tenant visible, authentification fonctionnelle.
2 frictions nouvelles sur cette session. Total : 33 frictions sur 11 sessions.
Session 12 — 5 juin 2026 — validation D1 (onglet Destinations)
Contexte : validation de D1 dans Sillage — parcours ajout/suppression de destinations de push via le nouvel onglet Destinations dans les réglages client.
Friction #34 — SoM : boutons dans <dialog> fermés occupent des slots invisibles
RÉSOLU le 5 juin 2026 (session 12 bis). Les trois fonctions SoM (
_SOM_INJECTER_JS,_SOM_COMPTER_HORS_VIEWPORT_JS,_SOM_TROUVER_JS) et le bloc inlineremplir_som SELECTont été corrigés dansshot.py: chaque élément est filtré par une traversée ascendante des ancêtres — si un ancêtre est un<dialog>sans attributopen, l’élément est ignoré. Les IDs SoM des quatre fonctions restent cohérents car le même filtre est appliqué partout.
Quand plusieurs <dialog> sont présents dans le DOM mais fermés, le SoM les numérote
dans sa liste globale de boutons. Le LLM cherche à cliquer “Supprimer” (confirm) dans la
dialog ouverte, mais le SoM pointe sur le bouton de la dialog suivante (fermée).
Symptôme : clic sur bouton “Supprimer” sans effet visible, ou ouverture de la mauvaise dialog.
Cause : le SoM ne distingue pas les éléments dans un <dialog> ouvert de ceux dans
un <dialog> fermé — il les indexe tous. Un <dialog> fermé est visuellement absent
mais présent dans l’arbre d’accessibilité.
Contournement appliqué (obsolète depuis la correction) : cibler par l’attribut id
de la dialog ouverte plutôt que par le numéro SoM.
1 friction nouvelle sur cette session. Total : 34 frictions sur 12 sessions.
Session 13 — 8 juin 2026 — première connexion à un service cloud multi-ports
Première connexion à __HOST_SERVICE__ (service Pretix hébergé sur plateforme
cloud). Vault configuré à ~/Vaults/Diwall/ avec des fichiers organisés en
sous-répertoires par service. Trois frictions vault découvertes lors de la
première tentative d’authentification via depuis_vault.
35. Structure plate uniquement dans vault.py — sous-dossiers ignorés silencieusement
Résolu en session 16 (spec rétroactive — _CADRE/SPECIFICATIONS/43_GROUPE_C_VAULT_FILL_PREUVES.md).
lib/vault.py effectue désormais une recherche récursive via os.walk(followlinks=False)
si aucun fichier n’est trouvé à la racine du vault. En cas d’ambiguïté (plusieurs candidats),
FileNotFoundError est levée avec la liste des fichiers trouvés — l’opérateur doit affiner
vault_dir dans diwall.conf.
36. Nommage hostname complet obligatoire, sans message d’erreur explicite
Contexte : fichier nommé __HOST_SERVICE_SLUG__.json (partie courte du
FQDN). vault.py attend exactement le résultat de urlparse(url).hostname,
soit __HOST_SERVICE__.json.
Symptôme : FileNotFoundError — aucune indication dans le message que
le nom de fichier doit correspondre au hostname complet. Le message existant
montre le chemin attendu, mais pas pourquoi le hostname est ce qu’il est.
Workaround : nommer le fichier avec le FQDN complet retourné par
urlparse(url).hostname. En cas de doute, vérifier en Python :
from urllib.parse import urlparse
print(urlparse("https://__HOST_SERVICE__").hostname)
# → __HOST_SERVICE__
Piste de correction : enrichir le message FileNotFoundError avec la
ligne explicite :
Nom attendu (d'après urlparse(url).hostname) : __HOST_SERVICE__.json
37. Collision hostname multi-services (port différent, même hôte)
Résolu en session 16 (spec rétroactive — _CADRE/SPECIFICATIONS/43_GROUPE_C_VAULT_FILL_PREUVES.md).
lib/vault.py applique un algorithme port-aware à 4 niveaux :
<hostname>_<port>.json (plat) → <hostname>.json (plat) → <hostname>_<port>.json (récursif) →
<hostname>.json (récursif). Chaque service sur un port distinct peut avoir son propre fichier
de credentials sans collision.
Synthèse session 13
3 frictions nouvelles (#35–#37), toutes liées au système de résolution vault. Cas d’usage déclencheur : hébergeur cloud avec FQDN long et plusieurs services sur des ports non standard partageant le même hostname.
Candidats prioritaires pour le prochain incrément :
- Recherche récursive optionnelle (#35) — ergonomie, organisation du vault
- Message d’erreur explicite avec hostname attendu (#36) — diagnostic immédiat
- Résolution port-aware (#37) — segmentation des credentials multi-services
37 frictions sur 13 sessions.
Session 14 — 9 juin 2026 — usage réel Pretix (authentification + navigation RGPD)
Session d’usage réel sur __HOST_SERVICE__ (service Pretix hébergé sur
plateforme cloud). Objectif : accéder aux paramètres RGPD globaux via
l’interface d’administration. Deux tentatives nécessaires, trois obstacles
avant d’atteindre le tunnel d’authentification.
38. --navigate documenté dans GUIDE_LLM.md mais absent de la version déployée
Contexte : le guide mentionne un flag CLI --navigate pour naviguer vers
une URL dans un appel rpa.py. La tentative d’utilisation retourne une erreur
de type « action inconnue » ou flag non reconnu.
Symptôme : l’opérateur suit la documentation officielle, l’outil rejette le flag. Aucun message ne signale que le flag est prévu mais non encore implémenté.
Cause probable : décalage entre une documentation prospective (ou rédigée pour une version future) et la version déployée sur la machine.
Workaround : utiliser l’action JSON {"type":"naviguer","url":"…"} dans
un tableau --actions, qui est fonctionnelle (confirmée depuis session 3,
friction #13).
Piste de correction : soit retirer --navigate du guide jusqu’à
implémentation, soit l’implémenter. Le guide ne doit pas documenter des
fonctionnalités absentes de la version de référence déployée.
39. diwall.conf unique par machine — conflit multi-projets sans résolution per-projet
Contexte : diwall.conf est stocké dans /opt/diwall/diwall.conf — un
seul fichier pour toute la machine. La clé vault_dir pointait sur le vault
du projet en cours (~/Vaults/<PROJET>/). Pour un second projet, il a fallu
contourner via la variable d’environnement DIWALL_VAULT_DIR=~/Vaults/Diwall
à chaque invocation.
Symptôme : sans le contournement, depuis_vault charge les credentials
du mauvais projet — erreur FileNotFoundError ou, pire, credentials incorrects
silencieusement injectés.
Impact : sur une machine hébergeant plusieurs projets Diwall (Sillage,
Pretix, client X…), la conf globale devient un point de friction permanent.
Le contournement DIWALL_VAULT_DIR= est fonctionnel mais doit être rappelé
à chaque invocation ou scriptés — source d’oubli.
Workaround : préfixer chaque invocation avec
DIWALL_VAULT_DIR=~/Vaults/<Projet> ou l’exporter en début de session shell.
Piste de correction : résolution per-projet — lire un fichier
.diwall.conf à la racine du répertoire courant (ou d’un répertoire parent)
en priorité sur /opt/diwall/diwall.conf. Compatible avec la cascade actuelle
(env var > conf local > conf global > défaut).
Lien avec friction #12 : même famille (vault path non trouvé), cause distincte (multi-projets vs chemin de vault inconnu de l’utilisateur).
40. /var/log/diwall/preuves — avertissement permission refusée à chaque run
Contexte : à chaque invocation de shot.py ou rpa.py, un avertissement
apparaît :
⚠ journal : preuves non archivées ([Errno 13] Permission denied: '/var/log/diwall/preuves')
Le run continue normalement, les captures vont dans /tmp/diwall/. L’opérateur
ne peut pas écrire dans /var/log/diwall/preuves car ce répertoire appartient
à root:diwall en mode 770 — et l’utilisateur ron n’a pas les droits
d’écriture directe.
Impact : l’avertissement est non-bloquant mais pollue toutes les sorties, crée de la confusion sur l’état du système, et masque les vraies erreurs dans le flux de logs.
Cause : /var/log/diwall/preuves est créé par install.sh avec des
permissions restrictives. L’utilisateur courant n’est pas dans le groupe
diwall au moment de la création, ou les permissions ne sont pas accordées
à l’utilisateur opérateur.
Workaround : sudo chmod 775 /var/log/diwall/preuves ou
sudo chown ron:diwall /var/log/diwall/preuves selon la politique locale.
Piste de correction : install.sh devrait accorder l’accès en écriture
à l’utilisateur opérateur sur ce répertoire (via usermod -aG diwall $USER
ou propriété $USER:diwall 775). À aligner avec la correction structurelle
suggérée en friction #33 (lib/ en 755).
Synthèse session 14
3 frictions nouvelles (#38–#40). Aucune n’est bloquante sur le fond — la connexion Pretix a finalement réussi en 2,4 s une fois les prérequis réglés — mais leur cumul (guide décalé, conf multi-projets, warning permission) représente une friction d’onboarding significative pour un nouvel opérateur.
Candidats prioritaires pour le prochain incrément :
- Résolution per-projet de
diwall.conf(#39) — impact direct multi-projets - Permissions
/var/log/diwall/preuvesdansinstall.sh(#40) — à aligner avec #33 - Alignement guide/version déployée pour
--navigate(#38) — cohérence doc
40 frictions sur 14 sessions.
Session 15 — 9 juin 2026 — authentification sudo et navigation Pretix
Suite de la session 14 sur __HOST_SERVICE__. Frictions liées au mécanisme
d’authentification sudo de Pretix et aux comportements Playwright lors de
navigations avec redirections enchaînées.
41. Perte du fichier session entre deux appels rapides
Contexte : /tmp/diwall/pretix-session.json disparaît ou devient invalide
si --sauver-session et --reprendre-session ciblent le même fichier en
succession rapide (moins de 10 s).
Cause probable : écriture partielle lors du --sauver-session — le fichier
est tronqué avant que --reprendre-session ne le lise.
Symptôme : JSONDecodeError ou session vide à la reprise, blocage
systématique si on tente de chaîner des appels.
Workaround : ne pas réutiliser le même fichier session entre deux appels rapides. Utiliser un nom horodaté, ou refaire la séquence complète depuis le login.
Piste de correction : écriture atomique dans shot.py pour
--sauver-session — écrire dans un fichier temporaire puis renommer
(write temp + rename), garantissant que le fichier cible est toujours
complet ou absent.
42. remplir (sélecteur CSS) inopérant sur <select> — alternative evaluer
Contexte : tentative de {"type":"remplir","selecteur":"select[name=X]","valeur":"Y"}.
page.fill() ne fonctionne que sur <input> et <textarea> — lève une erreur
sur un <select>.
Note : la friction #15 (résolue) couvrait remplir_som sur un <select>
via SoM ID — ce cas est distinct : remplir avec sélecteur CSS reste bloquant.
Workaround via evaluer :
{
"type": "evaluer",
"script": "const s=document.querySelector('select[name=X]'); s.value='Y'; s.dispatchEvent(new Event('change',{bubbles:true})); s.value"
}
Solution recommandée : préférer remplir_som avec l’ID SoM du <select>
(correction #15 déjà en production). remplir (CSS) sur <select> reste un
piège non documenté.
Piste de correction : documenter l’alternative evaluer dans GUIDE_LLM.md,
ou ajouter un message d’erreur explicite dans remplir quand l’élément cible
est un <select>.
43. Sélecteur button[type=submit] ambigu sur la page sudo
Contexte : page /control/sudo/ de Pretix contenant deux éléments
button[type=submit]. Le sélecteur Playwright button[type=submit] lève
une strict mode violation.
Workaround : button:has-text("Démarrer la session") — précis et stable.
Leçon : les pages d’authentification ont souvent plusieurs boutons submit (formulaire principal + formulaire caché de déconnexion). Toujours qualifier par le texte du bouton ou un attribut unique.
44. naviguer ERR_ABORTED vers URL déclenchant une chaîne de redirections sudo
Contexte : {"type":"naviguer","url":"/control/global/settings/"} échoue
avec net::ERR_ABORTED lorsque l’URL cible déclenche une chaîne de redirections
(settings → sudo → reauth). Playwright abandonne la navigation avant la
résolution finale.
Workaround : naviguer directement vers l’URL de reauth avec le paramètre
next correctement encodé, puis dérouler la séquence reauth + sudo en Mode A
unique depuis le login.
Leçon : Pretix (et probablement d’autres apps Django avec middleware sudo)
ne supportent pas la navigation directe vers les zones protégées depuis une
session normale. La séquence complète login → reauth → cible doit tenir dans
un seul appel Mode A.
45. URL organisateur : slash final → 404 (inconsistance)
Contexte : /control/organizer/__TENANT__/edit/ (avec slash final) → 404.
URL correcte : /control/organizer/__TENANT__/edit (sans slash final).
Inconsistance avec /control/global/settings/ qui accepte le slash final.
Impact : mineur — découvrable à la première tentative, message d’erreur lisible. À noter dans un scénario RPA pour éviter une double tentative.
Synthèse session 15
5 frictions nouvelles (#41–#45), toutes liées à l’usage réel de Pretix avec son mécanisme sudo Django et ses conventions d’URL.
Candidats pour le prochain incrément :
- Écriture atomique
--sauver-session(#41) — fiabilité critique - Message d’erreur
remplirsur<select>+ documentation (#42) — ergonomie - Frictions #43–#44 → à documenter dans
GUIDE_LLM.mdsection “parcours sudo”
45 frictions sur 15 sessions.
Session 16 — 9 juin 2026 — Vault multi-projet
Contexte : diwall.conf global contient un vault_dir en dur
(~/Vaults/<PROJET>/) — tous les projets utilisent le même coffre.
Demande : chaque projet doit pouvoir utiliser son propre coffre.
Décision architecturale : nouvelle cascade de configuration (planifiée avec
co-planification LLM, validée par l’opérateur) — voir _CADRE/SPECIFICATIONS/25_PHASE6_RPA_VAULT.md
section “Résolution du chemin vault” v1.1.
Frictions adressées : #39 (résolution per-projet) + #35/#37 (récursion + port-aware) intégrées dans un algorithme cohérent à 4 niveaux.
À implémenter : lib/vault.py (cascade DIWALL_CONF, algorithme 4 niveaux),
/opt/diwall/diwall.conf (remise à défaut), tests T_CONF_A–D.
47 frictions sur 16 sessions (frictions #39b et #39c ajoutées).
Session 17 — 9 juin 2026 — PHASE_VALIDATION multi-cibles + planification ergonomie
PHASE_VALIDATION : connexion à __HOST_SERVICE__, __HOST_DEMO__ et Sillage
pour récolter des données factuelles. Frictions #48–#51 toutes découvertes lors de
cette session.
48. Journal warning sur stdout — casse le parsing JSON en pipe
Contexte : invocation shot.py … | python3 -c "import json…" — le message
⚠ journal : log principal inaccessible apparaît sur stdout avant le JSON, cassant
le parsing.
Cause : certains cas d’erreur du journal émettent leur avertissement sur stdout au lieu de stderr.
Workaround :
result=$(/opt/diwall/venv/bin/python3 /opt/diwall/shot.py --url … 2>/dev/null | tail -1)
tail -1 récupère uniquement la dernière ligne (le JSON). 2>/dev/null élimine le stderr.
Piste de correction : rediriger systématiquement tous les avertissements non-JSON vers stderr.
49. --action inline avec JS complexe — casse sur les guillemets shell
Contexte : --action '{"type":"evaluer","script":"document.querySelector(\"#id\").value"}'
— l’imbrication de guillemets est interprétée par le shell avant d’atteindre shot.py.
Résultat : JSON invalide, erreur silencieuse ou évaluation vide.
Cause : le shell interprète les guillemets et les caractères spéciaux avant la transmission à Python.
Règle absolue : pour tout script JS contenant des guillemets ou des caractères
spéciaux, utiliser --actions /tmp/actions.json avec le tableau dans un fichier.
cat > /tmp/actions.json << 'EOF'
[{"type":"evaluer","script":"document.querySelector('#field').value"}]
EOF
/opt/diwall/venv/bin/python3 /opt/diwall/shot.py --url … --actions /tmp/actions.json
Lien : documenté dans GUIDE_LLM.md comme règle absolue.
50. naviguer dans une session reprise Django redirige vers le dashboard
Contexte : session __HOST_SERVICE__ reprise avec --reprendre-session, puis
{"type":"naviguer","url":"/control/organizer/__TENANT__/"} → redirection silencieuse
vers /control/ (dashboard), sans message d’erreur.
Cause : Django redirige vers le dashboard pour une navigation initiée depuis un contexte de session reprise (le middleware session détecte un contexte inhabituel).
Workaround : passer l’URL cible directement comme --url à l’invocation shot.py,
plutôt que via l’action naviguer dans une session reprise.
/opt/diwall/venv/bin/python3 /opt/diwall/shot.py \
--url https://__HOST_SERVICE__/control/organizer/__TENANT__/ \
--reprendre-session session.json --som
Lien : documenté dans GUIDE_LLM.md section “Known CLI pitfalls”.
51. Session sauvegardée avant fin de la redirection auth
Contexte : --sauver-session appelé immédiatement après le clic login sur
__HOST_SERVICE__. À la reprise, les cookies d’authentification ne sont pas encore
établis — session invalide.
Cause : même cause racine que friction #5 (session_regenerate_id, ou redirection
multi-étapes côté serveur). attendre_navigation ne garantit pas la propagation des
cookies de session côté serveur.
Règle : login + navigation post-login = séquence atomique unique. Ne jamais
insérer --sauver-session entre le submit du formulaire de login et la première
page authentifiée chargée.
[
{"type": "remplir_som", "id": 1, "valeur": "depuis_vault", "vault_cle": "username"},
{"type": "remplir_som", "id": 2, "valeur": "depuis_vault", "vault_cle": "password"},
{"type": "cliquer_som", "id": 3},
{"type": "pause", "ms": 2000},
{"type": "naviguer", "url": "https://__HOST_SERVICE__/control/"},
{"type": "capturer", "nom": "post-login"}
]
Lien : renforce friction #5 et #16.
Synthèse session 17
4 frictions nouvelles (#48–#51), toutes découvertes lors de la PHASE_VALIDATION multi-cibles. Frictions #48 et #49 sont des frictions d’ergonomie CLI (stdout pollution, shell escaping) ; frictions #50 et #51 sont des comportements Django lors des sessions reprises.
PHASE_PLANIFICATION co-Claude+Gemini a produit 6 chantiers (FR-47 à FR-53, FR-52
annulé) : sécurité symlinks vault, 4 primitives d’attente modernes, nettoyer_overlay,
mémoire sémantique ChromaDB/scénarios, fallback vector.py. Deux nouveaux documents
créés : GUIDE_EXPLORATION.md et GUIDE_HUMAIN.md.
51 frictions sur 17 sessions.
52. --actions (fichier) ignoré silencieusement en mode --reprendre-session
Contexte : login __HOST_SERVICE__ via --reprendre-session + --actions /tmp/fichier.json.
Les champs restent vides, le login échoue, succes: true est retourné sans erreur.
Cause : dans shot.py lignes 743–751, la branche reprendre_session ne lit que
args.action (inline). args.actions (fichier) est ignoré sans avertissement.
Règle : en mode --reprendre-session, toujours utiliser --action '[{...}]' (JSON
inline). --actions /fichier.json est réservé au mode --url (Mode A).
Fix : v1.8 unifiera les deux modes (FR-54).
Lien : documenté dans GUIDE_LLM.md section “Known CLI pitfalls” et dans CLAUDE.md.
53. attendre_url faux positif immédiat sur motif partiel
Contexte : après submit du formulaire de login Pretix, attendre_url "/control/"
retourne instantanément — la redirection post-login n’est jamais attendue.
Cause : page.wait_for_url("**/control/**") est une correspondance partielle.
L’URL courante /control/login/ contient déjà la sous-chaîne /control/.
Règle : ne jamais utiliser un motif qui est sous-chaîne de l’URL courante.
Après un submit de formulaire, préférer attendre_selecteur_present sur un élément
structurel présent uniquement sur la page post-login.
Lien : documenté dans GUIDE_LLM.md section “Known CLI pitfalls” (FR-55).
54. IDs SoM invalidés après mutation DOM (bandeau cookies, modals)
Contexte : acceptation du bandeau cookies sur __HOST_DEMO__ → cliquer_som 13
échoue : “élément SoM 13 non trouvé”. Le DOM a été modifié et les IDs ont été
renumérotés.
Cause : le SoM ré-indexe les éléments interactifs visibles dans le viewport à chaque capture. Toute action qui supprime ou ajoute des éléments DOM visibles (disparition du bandeau, ouverture/fermeture d’une modal) invalide tous les IDs précédents.
Règle : après toute action modifiant le DOM visible (dismiss cookie banner, fermeture
modal, overlay disparu), toujours exécuter un nouveau shot.py --som avant tout
cliquer_som ou remplir_som. Ne jamais réutiliser des IDs SoM entre deux états DOM.
Lien : documenté dans GUIDE_LLM.md section “Known CLI pitfalls” (FR-56).
55. Absence de pré-vol GUIDE_LLM.md en début de session
Contexte : en session 19, faute de lire GUIDE_LLM.md avant de commencer, le LLM a improvisé du scraping curl + extraction jq des credentials — violation de sécurité documentée.
Cause : Diwall n’est pas dans le corpus d’entraînement du modèle. Sans lecture explicite du guide, le comportement par défaut est l’improvisation.
Règle : lire /opt/diwall/docs/GUIDE_LLM.md en entier avant toute manipulation
Diwall. Ce pré-vol est maintenant inscrit dans CLAUDE.md (lu automatiquement par
Claude Code à chaque session) et dans PROTOCOLE_DEMARRAGE.md instruction n°1ter.
Lien : CLAUDE.md règles n°1–3, PROTOCOLE_DEMARRAGE.md instruction n°1ter (FR-57).
56. DIWALL_VAULT_DIR et DIWALL_CONF ont des sémantiques distinctes
Contexte : benchmark Gemini Flash (09/06/2026) — connexion à __TENANT_SERVICE__ +
__HOST_DEMO__ + Sillage. Gemini a positionné DIWALL_VAULT_DIR vers le répertoire
contenant le fichier .diwall.conf de Sillage. Vault introuvable → erreur silencieuse.
Cause : les deux variables existent dans vault.py mais ne pointent pas vers le même objet.
DIWALL_VAULT_DIR attend un répertoire contenant directement des fichiers <hostname>.json.
DIWALL_CONF attend un chemin vers un fichier .diwall.conf (JSON) dont la clé vault_dir
résout le répertoire vault. Pointer DIWALL_VAULT_DIR vers un répertoire qui contient un
.conf ne fonctionne pas — le .conf n’est pas lu.
Règle : pour tout projet utilisant un fichier .diwall.conf (configuration per-projet),
toujours utiliser DIWALL_CONF=/chemin/vers/fichier.diwall.conf. Réserver DIWALL_VAULT_DIR
aux coffres plats où les fichiers <hostname>.json se trouvent directement dans le répertoire pointé.
Lien : documenté dans GUIDE_LLM.md section “Known CLI pitfalls” (FR-58).
Synthèse session 19
5 frictions nouvelles (#52–#56), toutes découvertes lors de la PHASE_VALIDATION
multi-cibles (Pretix + __HOST_DEMO__ + Sillage), et lors du benchmark Gemini Flash.
Frictions #52 et #53 sont des bugs d’API de shot.py (comportement silencieux inattendu). Friction #54 est une règle d’usage SoM (extension de la règle scroll existante). Friction #55 est un défaut de protocole de démarrage. Friction #56 est une confusion sémantique entre deux variables d’environnement vault.
PHASE_DOCUMENTATION : 5 frictions documentées, CLAUDE.md créé, PROTOCOLE_DEMARRAGE.md
mis à jour (instructions n°1bis/1ter/1quater), GUIDE_LLM.md mis à jour (v1.8.0, bloc
sécurité en tête + 4 pitfalls). PHASE_EXECUTION : FR-54 et FR-55 corrigés dans shot.py
(commit 6982639). Benchmark Gemini Flash : exercice multi-cibles réussi sans fuite de données.
56 frictions sur 19 sessions.
57. Dialogues CSS Sillage — boutons bloqués par Playwright actionability
Session : 20 (11/06/2026) — PHASE_VALIDATION C2 Sillage 3.5.6.
Sillage utilise des dialogues de confirmation CSS (propriété display/visibility modifiée
via JS) plutôt que l’élément HTML <dialog open>. Playwright évalue l’interactabilité
d’un bouton en vérifiant sa visibilité dans le layout — un bouton CSS masqué échoue avec
Locator.click: Timeout 10000ms exceeded.
Éléments concernés : “Lancer le clonage”, “Supprimer définitivement”, tout bouton dans un dialogue Sillage ouvert par CSS.
Contournement systématique :
{
"type": "evaluer",
"script": "Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Lancer le clonage')?.click()"
}
.find().click() court-circuite les vérifications d’interactabilité Playwright — le click
JS est direct et ne dépend pas de la visibilité CSS.
Règle d’usage : pour tout bouton dans un dialogue Sillage, préférer evaluer + JS.
Ne pas tenter cliquer / cliquer_som en premier.
58. evaluer .value = ... cible l’élément ambigu quand le placeholder est réutilisé
Session : 20 (11/06/2026) — PHASE_VALIDATION C2 Sillage 3.5.6 (formulaire ajouter_domaine).
document.querySelector('input[placeholder="https://mon-site.fr"]') retourne le premier
élément correspondant dans le DOM. Sur la page Réglages/Domaines de Sillage, plusieurs
champs URL (domaines existants + formulaire NOUVEAU DOMAINE) partagent le même attribut
placeholder. Le querySelector a ciblé l’URL du premier domaine existant au lieu du champ
du formulaire d’ajout.
Conséquence : valeur https://test-c2.__DOMAINE_OPERATEUR__ injectée dans le champ URL de
__HOST_ADMIN__ (DOM uniquement). Aucune soumission serveur : validation HTML5
required a bloqué l’envoi car le vrai champ cible restait vide.
Fix : utiliser remplir_som après une recapture SoM pour cibler l’ID numéroté exact.
En alternative JS : Array.from(document.querySelectorAll('input[type="url"]')).pop() —
le dernier champ URL sur la page est le formulaire d’ajout.
Règle d’usage : evaluer .value = sur un champ ambiguë → toujours utiliser remplir_som
ou un sélecteur positionnel (last, nth-of-type) pour éviter le premier match.
Synthèse session 20
2 frictions nouvelles (#57–#58) — PHASE_VALIDATION C2 Sillage 3.5.6 (4 verbes base64 JSON).
Friction #57 : pattern récurrent pour Sillage — dialogs CSS sont invisibles à Playwright,
contournement JS systématique. Friction #58 : ambiguïté de sélecteur CSS quand le même
placeholder est réutilisé — préférer remplir_som pour les champs non uniques.
PHASE_VALIDATION C2 : 4 verbes validés (cloner, supprimer, calculer_metriques, ajouter_domaine). Unification base64 JSON Sillage 3.5.6 confirmée E2E.
58 frictions sur 20 sessions.
59. attendre_reseau_calme + opération serveur synchrone longue → timeout screenshot Playwright
Session : 26 (12/06/2026) — PHASE_VALIDATION E2E Jalon C, troisième passage (v1.9.3), remontée Claude Sillage.
Page.screenshot dans Playwright dispose d’un timeout interne fixé à 30 secondes, non configurable via l’option --timeout de rpa.py. Quand attendre_reseau_calme attend le calme réseau et que le serveur traite une opération synchrone longue (~1 min pour un clone PHP), le screenshot expire avant que l’opération ne se termine — le scénario s’interrompt sans erreur fonctionnelle côté serveur.
Ce n’est pas un bug Diwall. C’est une contrainte Playwright : le timeout de wait_for_load_state("networkidle") est contrôlable via timeout_ms, mais le screenshot qui suit dispose de son propre plafond non exposé.
Contournement validé : remplacer attendre_reseau_calme par pause dès que la durée serveur estimée dépasse ~20s. pause cède le contrôle du timing au scénario et ne déclenche pas de screenshot intermédiaire.
{"type": "evaluer", "script": "/* déclencheur opération longue */"},
{"type": "pause", "ms": 150000},
{"type": "capturer", "nom": "after_operation"}
Règle d’usage : ne jamais enchaîner attendre_reseau_calme sur un déclencheur dont la durée serveur peut dépasser ~20s. capturer est compatible avec les longues attentes — il ne déclenche pas de timeout Playwright autonome.
60. evaluer mutant dispatché avant le timeout Diwall → artefact serveur parasite
Session : 26 (12/06/2026) — PHASE_VALIDATION E2E Jalon C, troisième passage (v1.9.3), remontée Claude Sillage.
evaluer envoie l’instruction JavaScript à la page immédiatement, avant toute action attendre_* qui suit dans la liste. Si le scénario échoue ensuite (timeout sur attendre_reseau_calme, screenshot expiré), l’opération déclenchée côté serveur a déjà démarré ou s’est terminée. Diwall ne peut pas annuler une action déjà dispatchée au serveur.
En pratique : un evaluer cliquant un bouton “Lancer le clonage” + un timeout Diwall sur l’attente suivante = un clone créé côté serveur, non détecté par le scénario. La relance naïve du scénario peut créer un second clone.
Ce n’est pas un bug Diwall. C’est une propriété fondamentale de l’architecture : Diwall est un exécuteur d’actions sans mécanisme de rollback. Les mutations serveur sont définitives dès l’envoi du signal JS.
Règle d’usage : après tout scénario raté contenant une action mutante (evaluer sur bouton déclencheur, remplir_som + cliquer_som sur formulaire), vérifier l’état du serveur avant relance :
- Capturer la page cible en Mode A sans actions
- Confirmer si l’opération a démarré, est en cours, ou n’a pas eu lieu
- Reprendre le scénario uniquement à partir du point après la mutation réussie
Signal d’alerte : si le log journal (journal.py --cible target --mutatif) enregistre une opération mutatrice sur la cible dans les minutes précédant l’échec, l’opération a probablement abouti côté serveur malgré le timeout Diwall.
Synthèse session 26
2 frictions nouvelles (#59–#60) — découvertes lors de la PHASE_VALIDATION E2E Jalon C (v1.9.3), remontées par Claude Sillage.
Friction #59 : limitation Playwright non exposée (timeout screenshot 30s fixe) — contournement pause documenté. Friction #60 : propriété architecturale de Diwall (pas de rollback sur les actions mutantes) — règle de vérification d’état serveur avant relance.
Les deux frictions ont été documentées dans GUIDE_LLM.md (section Known CLI pitfalls) et dans ce fichier. Elles complètent le tableau des comportements non évidents de l’architecture Diwall × Playwright.
Friction #61 — Checkbox masquée CSS : cliquer → timeout systématique
Découverte : Campagne de test E2E Sillage v3.5.6, 2026-06-14. Fonctionnalité WP1/WP2 (toggle WP_DEBUG).
Contexte : <input type="checkbox"> masqué par CSS (classe toggle-switch) — l’élément est dans le DOM, visible à l’inspection, mais son layout CSS le rend hidden au sens de Playwright.
Symptôme : {"type": "cliquer", "selecteur": "[data-sillage='toggle-wp-debug']"} → timeout après 24 tentatives, même avec --timeout 15000. Aucun message d’erreur explicite — juste “element is hidden”.
Pattern obligatoire :
{"type": "evaluer", "script": "document.querySelector('[data-sillage=\"toggle-wp-debug\"]').click()"}
Règle : Tout <input> masqué par CSS (pattern toggle-switch, checkbox hidden) → evaluer direct, sans essai cliquer préalable.
Friction #62 — <select> avec guard JS silencieux : clic sans effet
Découverte : Campagne de test E2E Sillage v3.5.6, 2026-06-14. Fonctionnalité D6 (suppression en lot).
Contexte : Un bouton btn-appliquer-lot dont la callback JS (ouvrirDialogLot()) retourne silencieusement si select-action-lot.value === "" (valeur par défaut). Playwright exécute le clic sans erreur — la page ne bouge pas.
Symptôme : cliquer sur le bouton conditionnel → succès Playwright, aucun dialog, aucune erreur.
Pattern obligatoire :
[
{"type": "evaluer", "script": "document.querySelector('[data-sillage=\"select-action-lot\"]').value = 'supprimer'"},
{"type": "cliquer", "selecteur": "[data-sillage='btn-appliquer-lot']"},
{"type": "attendre_selecteur_present", "selecteur": "dialog#dialog-lot[open]"}
]
Règle : Avant tout clic conditionnel à la valeur d’un <select>, forcer la valeur via evaluer et vérifier l’effet avec attendre_selecteur_present. Un clic “réussi” ne prouve pas que son effet est visible.
Friction #63 — Bouton dans <dialog> HTML natif : cliquer timeout
Découverte : Campagne de test E2E Sillage v3.5.6, 2026-06-14. Fonctionnalité D6 (annuler la suppression en lot).
Contexte : btn-annuler-lot est dans un <dialog> ouvert via showModal() — element HTML natif, pas une modale CSS. Malgré l’attribut open présent sur le dialog (confirmé par attendre_selecteur_present), Playwright refuse le cliquer.
Symptôme : cliquer sur btn-annuler-lot → timeout, même après attendre_selecteur_present sur dialog#dialog-lot[open].
Pattern obligatoire :
{"type": "evaluer", "script": "document.querySelector('[data-sillage=\"btn-annuler-lot\"]').click()"}
Règle : evaluer est nécessaire pour les boutons dans tout conteneur ouvert via JS (CSS show/hide comme FR-57, ou <dialog> natif via showModal()). Le pattern général : si l’élément parent a été ouvert/affiché via JS, ne pas tenter cliquer — aller directement à evaluer.
Synthèse session 27
3 frictions nouvelles (#61–#63) — découvertes lors de la campagne de test E2E v3.5.6 Sillage (2026-06-14, 32 fonctionnalités validées sur 45).
Thème commun : les éléments DOM interactifs masqués ou ouverts via JS résistent à cliquer même quand ils sont “présents” dans le DOM. evaluer est le seul pattern fiable pour (1) les inputs CSS cachés, (2) les boutons conditionnels à un état JS, (3) les boutons dans des <dialog> HTML natifs.
Ces frictions étendent et complètent FR-57 (modales CSS Sillage).
60 frictions sur 26 sessions.
Friction #64 — Chemin Python RAG : /opt/diwall/venv n’a pas chromadb
RÉSOLU le 15/06/2026 (session 30). Chemin corrigé dans
PROTOCOLE_DEMARRAGE.md,PROTOCOLE_CLOTURE.md,scripts/build-index.pyetscripts/search-index.py(_CADRE).
Session 28 — 2026-06-15
Symptôme : ModuleNotFoundError: No module named 'chromadb' en tentant :
/opt/diwall/venv/bin/python3 ~/git/Diwall/_CADRE/scripts/search-index.py "..."
Cause : /opt/diwall/venv ne contient que playwright. Il ne sert qu’à rpa.py,
watch.py et shot.py. Il n’a jamais eu chromadb.
Chemin correct (vérifié 2026-06-15) :
~/.pyenv/versions/3.12.11/bin/python3 ~/git/Diwall/_CADRE/scripts/search-index.py "..."
Ce Python a chromadb, et la base de Diwall (Diwall/_CADRE/MEMOIRE/chroma_db/) lui est accessible.
Le GUIDE_LLM.md et tous les fichiers de documentation Diwall référencent la mauvaise commande.
Contournement immédiat : utiliser ~/.pyenv/versions/3.12.11/bin/python3.
Action requise (Claude Diwall) : corriger le chemin dans GUIDE_LLM.md et dans les
commentaires d’en-tête de build-index.py / search-index.py du dépôt Diwall.
Synthèse session 28
1 friction nouvelle (#64) — mauvais chemin Python pour le RAG Diwall.
Constat fait lors de la mise à jour du RAG Sillage (ajout type fondateur).
61 frictions sur 27 sessions.
Friction #65 — Matomo SitesManager : sélecteurs Vue.js vs hypothèse AngularJS
Session 30 — 2026-06-15
Objectif : Créer le site __DOMAINE_OPERATEUR__ dans Matomo via Diwall.
Durée totale : 32 minutes 25 secondes.
Résultat : succès — site ID 7 créé, tracker injecté et déployé.
Cause racine — mauvais sélecteur, hypothèse de framework erronée
Le LLM a supposé que Matomo utilisait AngularJS (nom de classe ng-click, interface
similaire) sans vérifier. En réalité typeof angular === 'undefined' dans le DOM —
Matomo SitesManager (v4+) utilise Vue.js.
Conséquence directe : les tentatives de mise à jour du modèle via angular.element().triggerHandler()
ont échoué silencieusement. Et les evaluer + dispatchEvent ne déclenchent pas
la réactivité Vue.
Piège #1 — button.addSite n’existe pas
Le LLM a répété button.addSite plusieurs fois avant de diagnostiquer.
Le bouton est un <a class="btn addSite">, pas un <button>.
CSS text-transform: uppercase affiche “AJOUTER” dans l’UI — le DOM contient
“Ajouter”. Cibler par texte uppercase → n’importe quel autre élément.
❌ {"type": "cliquer", "selecteur": "button.addSite"} → TimeoutError
✅ {"type": "cliquer", "selecteur": ":nth-match(a.addSite, 1)"}
Baisse de concentration identifiée : le LLM a relancé le même sélecteur échoué
au lieu de diagnostiquer avec evaluer pourquoi l’élément n’était pas trouvé.
Diagnostic correct : Array.from(document.querySelectorAll('[class*="add"]')).
Piège #2 — input[role='validation'] n’est pas le bouton Enregistrer
Deux input.btn coexistent dans la page :
| Élément | Contenu | Visible |
|---|---|---|
input[type=button][value='Envoyer le retour'] | Dialog feedback de Matomo (sondage) | Non |
input[type=submit][value='Enregistrer'] | Footer du formulaire de site | Oui |
input.btn → strict mode Playwright (2 éléments) → TimeoutError.
input[role='validation'] → cible le bouton du sondage (caché) → visible=false → TimeoutError.
❌ {"type": "cliquer", "selecteur": "input.btn"} → strict mode
❌ {"type": "cliquer", "selecteur": "input[role='validation']"} → visible=false
✅ {"type": "cliquer", "selecteur": "input[value='Enregistrer']"}
Diagnostic utile :
Array.from(document.querySelectorAll('input.btn')).map((el,i)=>
i+'|visible='+(el.offsetParent!==null)+'|value='+el.value+'|parent='+el.parentElement?.className?.slice(0,40)
).join('\n')
Piège #3 — evaluer + dispatchEvent ne suffit pas pour Vue.js
Les champs #siteName et #urls ne transmettent pas leur valeur au modèle
Vue si on les remplit via evaluer + dispatchEvent. Vue.js écoute ses propres
événements synthétiques, pas ceux du DOM natif.
❌ evaluer → element.value = 'x'; element.dispatchEvent(new Event('input',{bubbles:true}))
✅ {"type": "remplir", "selecteur": "#siteName", "valeur": "__DOMAINE_OPERATEUR__"}
La primitive remplir utilise Playwright fill() qui déclenche les bons événements
pour React, Vue, et les frameworks modernes.
Piège #4 — pause trop courte après navigation
Avec pause: 3000, Vue.js n’a pas terminé de monter les composants SitesManager.
document.querySelector('a.addSite') retourne null. Résultat : attendre_selecteur_present
timeout sur a.addSite (car le sélecteur testé était button.addSite — double erreur).
❌ pause 1500 ms après naviguer → composants Vue pas montés
✅ pause 4000 ms après naviguer → liste de sites visible, a.addSite présent
Séquence validée
Voir _CADRE/SPECIFICATIONS/PROCEDURES_LLM/TACHE_matomo-ajouter-site.md
pour la procédure complète avec toutes les étapes.
Règle extraite
Avant toute interaction avec une interface web inconnue via Diwall :
- Vérifier le framework :
typeof angular,typeof Vue,typeof React- Diagnostiquer l’élément exact :
document.querySelector('[class*="addSite"]')?.tagName- Ne jamais répéter un sélecteur qui a échoué sans diagnostic intermédiaire
Synthèse session 30
1 friction nouvelle (#65) — Matomo SitesManager, sélecteurs Vue.js, baisses de concentration.
Découverte lors de l’ajout du tracker Matomo sur __DOMAINE_OPERATEUR__.
Fiche opératoire créée : TACHE_matomo-ajouter-site.md.
62 frictions sur 30 sessions.
Session 31 — 18 juin 2026 — Validation C1b Sillage (UI admin maître)
Validation E2E de C1b (gestion des locataires multi-tenant) via rpa.py --scenario.
14/14 assertions passées. Une friction identifiée lors des runs de mise au point.
66. attendre_absence timeout sur la première soumission de formulaire
Contexte : scénario qui, depuis ?vue=login, remplit le formulaire de login et
soumet. C’est la première action POST de toute la session Playwright.
Séquence défaillante :
{"type": "remplir", "selecteur": "input[name=\"identifiant\"]", "valeur": "depuis_vault", "vault_cle": "username"},
{"type": "remplir", "selecteur": "input[name=\"password\"]", "valeur": "depuis_vault", "vault_cle": "password"},
{"type": "cliquer", "selecteur": "button.login-bouton"},
{"type": "attendre_absence", "selecteur": "input[name=\"identifiant\"]"}
← TimeoutError 10000ms, 24 polls, element still present
Le login RÉUSSIT côté serveur (vérifié via curl et password_verify). La session PHP
session_regenerate_id(true) émet un nouveau cookie. Playwright suit le redirect → ? →
dashboard. Mais attendre_absence commence à poller AVANT que Playwright ne commence à
traiter la réponse de la redirection, et chaque poll voit l’élément encore présent (page
de login toujours chargée dans le contexte courant). Après 10s, timeout.
Ce qui le révèle : le même pattern (attendre_absence sur input[name="identifiant"])
FONCTIONNE dans un autre scénario (C1a valider_auth_multitenant.json) parce que deux
soumissions de formulaire (avec attendre_url "err=1") ont eu lieu AVANT — Playwright
a « pré-chauffé » sa gestion des navigations POST → redirect.
Séquence fonctionnelle :
{"type": "cliquer", "selecteur": "button.login-bouton"},
{"type": "pause", "ms": 2000},
{"type": "evaluer", "script": "!window.location.href.includes('vue=login')", "attendu": true}
Règle : sur la PREMIÈRE soumission de formulaire d’un scénario (première navigation
POST), ne pas utiliser attendre_absence ni attendre_navigation immédiatement après
le clic — insérer un pause ms:2000 suivi d’un evaluer sur l’URL cible.
Lien avec friction #5 et #16 : même famille — session_regenerate_id() post-login
- timing Playwright. Ici, l’angle nouveau est que le problème est spécifique à la première soumission d’un scénario (pas les suivantes).
Synthèse session 31
1 friction nouvelle (#66). C1b Sillage 14/14 assertions validées.
Règle documentée dans docs/GUIDE_LLM.md v2.3.
63 frictions sur 31 sessions.
Friction #67 — pause fixe : borne temporelle arbitraire remplaçable par attendre_selecteur_present
Remontée par : Claude Sillage (session 32, 20/06/2026), à la suite de la campagne C1b.
Catégorie : ergonomie / fiabilité scénarios — aucun changement API Diwall requis.
Problème : pause ms:N est utilisé comme rustine post-action alors qu’un élément DOM précis
signale l’état attendu. C’est une borne temporelle arbitraire :
- sur un serveur rapide, on attend le pire cas inutilement ;
- sur un serveur lent, si la durée réelle dépasse N ms, le scénario continue sur un état incohérent.
Cause racine : les pauses ont été introduites par itération défensive, faute d’un sélecteur
connu au moment de l’écriture. Le pattern s’est reproduit dans valider_admin_maitre_c1b.json
(11 occurrences).
Correction appliquée en v1.9.8 : 10 des 11 pauses remplacées dans valider_admin_maitre_c1b.json :
| Cat | Avant | Après |
|---|---|---|
| A (post-login ×2) | pause ms:2000 | attendre_absence input[name="identifiant"], delai_initial_ms:500 |
| B (navigation + body ×3) | attendre_selecteur_present body + pause ms:600 | attendre_selecteur_present [data-sillage="toggle-creer-locataire"] |
| C1/C2 (post-AJAX ×2) | pause ms:2000 | attendre_selecteur_present [data-sillage="mdp-temp-locataire"] |
| C3 (post-suppression) | pause ms:2000 | attendre_absence tr[data-sillage="ligne-tenant-<id>"] |
| D (dialog open ×2) | pause ms:400 | attendre_selecteur_present #dialog-id[open] |
E (animation <details>) | pause ms:300 | attendre_selecteur_present input[name="nouveau_tenant"] |
Note C3 : l’attribut data-sillage="ligne-tenant-<id>" a été ajouté dans page_tenant.php
par Claude Sillage (commit Sillage a762dbe) pour rendre la suppression attendable.
Règle : pause est réservé aux transitions CSS pures (aucun changement DOM) et aux
opérations serveur longues sans signal DOM. Dans tous les autres cas, attendre_selecteur_present
(ou attendre_absence pour les disparitions) est la primitive correcte.
Corollaire : attendre_selecteur_present body + pause N est un anti-pattern — body est
toujours attaché, la combinaison est équivalente à une pause pure. Remplacer par un sélecteur
sur un élément de contenu métier.
Synthèse session 32
1 friction nouvelle (#67 — pauses fixes → attentes sémantiques). Trilatérale l’opérateur / Claude Diwall /
Claude Sillage. Vérification PHP par Sillage, commit Sillage a762dbe.
GUIDE_LLM.md v2.4. valider_admin_maitre_c1b.json mis à jour. v1.9.8 livré et validé.
64 frictions sur 32 sessions.
Note 19/06/2026 — Lacune : HOST_ADMIN inaccessible depuis Diwall (cert auto-signé)
Contexte : validation migration HOST_VPS Phase 1 — tentative de capture https://__HOST_ADMIN__/
depuis la machine de développement via shot.py (Playwright Chromium).
Comportement : succes: false, http_status absent. Playwright rejette le certificat
auto-signé de __HOST_ADMIN__ sans option de contournement exposée par shot.py.
Ce qui manque dans Diwall : une option --ignore-https-errors pour les environnements
locaux avec cert auto-signé (développement, staging intranet). Sans cette option, Diwall
est inutilisable sur les interfaces locales HTTPS non certifiées par Let’s Encrypt.
Contournement actuel : validation par WP-CLI en SSH direct sur HOST_VPS.
Impact : Sillage sur HOST_ADMIN ne peut pas être testé visuellement avec Diwall.
Sillage sur __DOMAINE_OPERATEUR__ (cert LE valide) est accessible.
Note 18/06/2026 (hors session Diwall) — Lacune signalée : bwlimit rsync
Contexte : chantier 3 Sillage — push DOMAINE_OPERATEUR HOST_ADMIN→HOST_VPS via connexion internet.
Lacune identifiée : le script générique transfert-wordpress-local-vers-serveur.sh
utilise rsync sans --bwlimit. Sur une connexion internet, un débit non limité cause un
Broken pipe après ~1.2 Mo transférés (buffers SSH saturés).
Ce qui manque dans Sillage : une variable srv_<alias>_bwlimit dans config.sh,
passée par le wrapper local-vers-serveur.sh comme variable d’environnement, et utilisée
dans le script générique comme --bwlimit=${SILLAGE_RSYNC_BWLIMIT:-}.
Contournement actuel : rsync manuel avec --bwlimit=500 avant le push officiel.
Cette lacune est propre à Sillage, pas à Diwall. Consignée ici par erreur de routage initial.
(→ Reporter dans _CADRE/MEMOIRE/RETOUR_EXPERIENCE.md de Sillage si besoin de suivi.)
Friction #68 — diwall.conf inaccessible après installation : 640 root:root ou groupe inactif
Remontée par : Claude Sillage (session 36, 21/06/2026).
Catégorie : installation / permissions — silencieux (exit 43).
Problème : deux causes indépendantes produisent le même symptôme (VaultNonConfigureError, exit 43) :
root:root 640au lieu deroot:diwall 640—deploy.shfaitsudo chown root:"$GROUPE" diwall.conf 2>/dev/null || true. Si le groupediwalln’existe pas encore au moment du déploiement (ex.deploy.shlancé sansinstall.shpréalable), lechownéchoue silencieusement → propriétaireroot:root→ronne peut pas lire le fichier.Groupe
diwallinactif dans la session courante —install.shajouteronau groupe viausermod -aG diwall ron, mais ce changement n’est effectif qu’à la prochaine reconnexion. Sirpa.pyest lancé dans la même session,ronn’a pas encore le groupe → lecture interdite → exit 43.
Symptôme : VaultNonConfigureError avec exit 43 — le message ne précise pas si la cause est une permission ou une configuration manquante.
Contournement immédiat :
# Cas 1 — mauvais propriétaire
sudo chown root:diwall /opt/diwall/diwall.conf
# Cas 2 — groupe inactif (sans reconnexion)
sg diwall -c "/opt/diwall/venv/bin/python3 /opt/diwall/rpa.py ..."
# ou se reconnecter
Cause racine : install.sh vérifie les permissions des répertoires (check_dir) mais pas des fichiers (diwall.conf, diwall-sample.conf). Le message d’erreur VaultNonConfigureError ne distingue pas “fichier illisible” de “fichier absent ou vault_dir non configuré”.
Correction à terme : ajouter une vérification check_file pour diwall.conf dans install.sh + préciser le message d’erreur selon la cause (IOError vs JSON invalide).
Friction #69 — Message d’erreur schéma JSON non orientant : “is not of type ‘object’”
Remontée par : Claude Sillage (session 36, 21/06/2026).
Catégorie : ergonomie / débogage scénarios.
Problème : quand la racine du fichier JSON n’est pas un objet ({"actions": [...]}) mais un tableau ([...]) ou une autre structure, rpa.py retourne :
ValidationError: [...] is not of type 'object'
Le message ne dit pas quelle structure est attendue, ni quel champ manque. Un LLM qui débogue perd du temps à comprendre que la racine doit être {"actions": [...]} et pas directement [...].
Contournement : structure obligatoire rappelée dans GUIDE_LLM.md — envelopper la liste d’actions dans {"actions": [...]}.
Correction à terme : intercepter ValidationError au niveau racine et émettre un message explicite :
Erreur schéma : la racine du scénario doit être un objet {"actions": [...]}
Reçu : tableau (list) — enveloppez vos actions dans {"actions": [...]}.
Friction #70 — remplir_som vide le champ avant saisie : comportement non documenté dans GUIDE_LLM
Remontée par : Claude Sillage (session 36, 21/06/2026). Découvert en lisant les changelogs, pas dans la documentation.
Catégorie : documentation / comportement inattendu.
Problème : depuis v1.9.6, remplir_som efface le champ via document.activeElement.value = '' + dispatch input avant la saisie, au lieu du triple-clic historique. Ce comportement n’est pas décrit dans le tableau des actions du GUIDE_LLM.md.
Conséquence : un LLM qui s’attend à de la concaténation (ex. champ pré-rempli + saisie additionnelle) sera surpris. La découverte se fait à l’exécution ou par lecture des changelogs.
Correction appliquée (session 36) : ajout d’une note dans le tableau remplir_som du GUIDE_LLM.md — “clears the field before typing (v1.9.6+)”.
Friction #71 — --secrets : logistique du fichier credentials dans le vault à chaque run
Remontée par : Claude Sillage (session 36, 21/06/2026).
Catégorie : ergonomie / workflow --secrets.
Problème : --secrets <fichier> exige que le fichier soit dans un répertoire qui est un point de montage actif (/proc/mounts). Un fichier dans /tmp est refusé par le contrôle T1 (VaultFermeError(42)). Cela implique :
- Avoir le vault gocryptfs monté au moment du run.
- Maintenir un fichier credentials JSON par tenant dans ce vault (création initiale manuelle).
- Si le vault n’est pas monté → le run échoue dès le pré-check.
La logistique (monter le vault, vérifier que le fichier existe) est invisible dans la commande rpa.py et génère des surprises à l’exécution.
Contournement recommandé : maintenir un fichier JSON permanent par tenant dans le vault (ex. ~/Vaults/<PROJET>/Diwall/tenant_alpha.json). Le format attendu :
{
"username": "...",
"password": "...",
"totp_cle": "SEED_BASE32_SI_MFA"
}
Seules les clés référencées dans le scénario sont requises.
Correction à terme : émettre un message d’erreur explicite quand le vault n’est pas monté, distinct du refus /tmp — distinguer “répertoire non monté” de “répertoire non sécurisé”.
Friction #72 — _coffre_est_monte : sous-dossier d’un coffre refusé à tort (dérive sémantique T1)
Remontée par : Gemini (analyse de session Sillage, 21/06/2026). Friction vécue par Claude Sillage.
Catégorie : sécurité / bug silencieux — VaultFermeError(42) sur un chemin légitime.
Problème : _coffre_est_monte(vault_dir) dans lib/vault.py fait :
any(chemin in ligne for ligne in /proc/mounts)
Ce test cherche chemin comme sous-chaîne de chaque ligne de /proc/mounts. Cela fonctionne
quand vault_dir EST le point de montage exact (ex. ~/Vaults/<PROJET>). Mais si le
fichier credentials est dans un sous-dossier du coffre monté (ex.
~/Vaults/<PROJET>/Diwall/__TENANT__.json), le répertoire parent est
~/Vaults/<PROJET>/Diwall — absent de /proc/mounts (seul
~/Vaults/<PROJET> y figure). Le test retourne False → VaultFermeError(42).
Contournement de Sillage : copier le fichier credentials à la racine du coffre
(~/Vaults/<PROJET>/__TENANT__.json) pour que le répertoire parent soit
exactement le point de montage.
Cause racine : dérive sémantique — la vérification testait l’égalité exacte du chemin au lieu de tester si le chemin est sous un point de montage actif.
Correction appliquée (session 36) : _coffre_est_monte parse désormais la colonne 2
de /proc/mounts (chemin de montage) et vérifie si chemin est le point de montage ou
en est un sous-dossier (chemin.startswith(point + "/")). Restriction aux systèmes de
fichiers FUSE ("fuse" in fstype) pour ne pas affaiblir T1 en acceptant des sous-dossiers
de systèmes de fichiers persistants ordinaires (ext4, btrfs, etc.).
Synthèse session 36
4 frictions nouvelles (#68 — diwall.conf permissions, #69 — message schéma JSON, #70 — remplir_som clear implicite, #71 — –secrets logistique vault). Remontées par Claude Sillage. Corrections documentation : GUIDE_LLM.md étendu (tail -1 pour rpa.py + diagnostic hint cliquer timeout).
68 frictions sur 36 sessions.
Friction #73 — capturer expire pendant une opération serveur synchrone longue
Remontée par : <LLM_PARTENAIRE> (session 36, 21/06/2026 — suite, après-midi). Découverte en conditions réelles lors d’un parcours utilisateur complet (persona « Pierre »).
Contexte. Un clic déclenche une opération synchrone longue côté serveur (clonage
WordPress : export distant + ~70 tables + rsync, plusieurs minutes). La requête POST ne
rend jamais networkidle tant qu’elle n’est pas terminée.
Symptôme. Une action capturer placée après attendre_reseau_calme échoue :
Page.screenshot: Timeout 30000ms exceeded.
- taking page screenshot
- waiting for fonts to load...
- fonts loaded
Le screenshot Playwright attend la stabilité de rendu (polices, layout) ; sur une page
bloquée au milieu d’une opération, ce délai (30 s) expire avant que la page ne se stabilise.
Conséquence : impossible d’observer l’écran pendant l’opération avec un capturer terminal,
alors même que c’est le moment qu’on veut justement valider (retour visuel de progression).
Contournement qui fonctionne. interval_capture pendant un pause produit des images
intermédiaires sans attendre la stabilité finale :
{"type": "pause", "ms": 180000, "interval_capture": 5}
Le capturer terminal, lui, ne réussit qu’une fois la page réellement stabilisée (après
rechargement, l’état final se capture sans problème).
Pistes (lacune DX, pas bug bloquant) :
- Doc « Pièges courants » du
GUIDE_LLM.md: pour observer une opération synchrone longue, utiliserpause+interval_capture— ne pas compter sur uncaptureraprèsattendre_reseau_calmetant que la page n’a pas rendu networkidle. - Option sur
capturer: un mode « capture d’état en cours » qui borne ou désactive l’attente de stabilité (waiting for fonts), pour figer une page volontairement « busy ». Utile à tout testeur d’opérations longues.
Note connexe (cf #69). Au cours du même parcours, {"type": "attendre", "ms": 800} a été
rejeté (is not valid under any of the given schemas) — attendre prend selecteur, c’est
pause qui prend ms (erreur d’usage de ma part). #69 a traité le cas général du message
non orientant ; suggestion ciblée : étendre le hint « clé mal placée » pour mapper ms sur
attendre → suggérer pause, comme c’est déjà fait pour attendu sur attendre.
Corrigé en v1.10.2. rpa.py détecte désormais la combinaison type: attendre + clé ms
et émet : “→ attendre attend un sélecteur CSS (selecteur). Pour un délai fixe, utilisez pause.”
Synthèse session 37
1 friction nouvelle (#73 — capturer expire pendant opération serveur synchrone longue). Remontée par <LLM_PARTENAIRE> lors d’un parcours complet persona « Pierre ».
Corrections : GUIDE_LLM.md FN7 corrigé (affirmation fausse supprimée, pattern pause+interval_capture ajouté) ; rpa.py hint ciblé attendre+ms → suggère pause (note connexe FR-73/FR-69).
74. Session éphémère détruite entre deux appels successifs
Session : 38 — Testeur : Gemini 3.5 Flash — Mission : audit découverte interface Sillage — 23/06/2026
shot.py supprime silencieusement le fichier de session passé via --reprendre-session à
la fin de l’exécution, sauf si --sauver-session est également spécifié dans le même appel.
Comportement observé : un agent effectuant plusieurs lectures successives (page A → page B) en réutilisant la même session perd l’accès à sa session après le premier appel si elle n’est pas explicitement resauvegardée. Résultat : ré-authentification forcée à chaque étape.
Piste suggérée par le testeur : ne jamais supprimer un fichier de session existant par
défaut lors d’une simple reprise (--reprendre-session sans --sauver-session).
La suppression devrait être réservée aux sessions générées à la volée (sans chemin explicite).
Ou ajouter une option --keep-session.
Corrections (v1.11.1 — session 39, 23/06/2026) : _nettoyer_session_ephemere désactivée dans
shot.py — la suppression silencieuse est retirée. T-A1 VERT.
75. Nettoyage de /tmp/diwall/ détruit la session qui y est stockée
Session : 38 — Testeur : Gemini 3.5 Flash — Mission : audit découverte interface Sillage — 23/06/2026
À chaque démarrage, shot.py nettoie son dossier de sortie par défaut /tmp/diwall/.
Si l’utilisateur a sauvegardé sa session dans ce répertoire (chemin naturel :
--sauver-session /tmp/diwall/session.json), le fichier est effacé par le run suivant
avant que Playwright n’ait pu le lire.
Comportement observé : FileNotFoundError sur --reprendre-session /tmp/diwall/session.json
alors que le fichier avait bien été créé par l’appel précédent.
Piste suggérée par le testeur : protéger les fichiers .json du nettoyage automatique
de /tmp/diwall/, ou imposer un sous-dossier dédié (ex. /tmp/diwall/sessions/) exclu
du nettoyage. Le testeur a contourné en passant le chemin de session hors de /tmp/diwall/.
Analyse post-correction (session 39) : shot.py n’effectue aucun rmtree sur /tmp/diwall/.
La cause racine est identique à FR-74 — _nettoyer_session_ephemere supprimait le fichier en
fin de run. FR-75 est une manifestation de FR-74 avec un chemin dans /tmp/diwall/.
Corrections (v1.11.1 — session 39, 23/06/2026) : même correctif que FR-74. T-B1 VERT.
Synthèse session 38
2 frictions nouvelles (#74 et #75 — gestion de session entre appels successifs). Remontées par Gemini 3.5 Flash lors de la mission « audit découverte interface Sillage » (première mission Famille A exécutée par un testeur LLM externe au projet).
71 frictions sur 38 sessions.
69 frictions sur 37 sessions.
Inter-session Sillage — 25 juin 2026 — FR-76 : naviguer post-submit inutilisable
Contexte : validation Diwall C4 (dispatcher compilé shc) sur Sillage. Scénario valider_c4_cloner_client_sanctuarise.json — clonage WP via evaluer click() sur le bouton submit d’un <dialog> natif.
FR-76 — naviguer après evaluer click() sur submit → ERR_ABORTED ou Timeout
Description : après un evaluer qui déclenche un .click() sur un bouton <button type="submit"> (submit de formulaire), toute action naviguer suivante échoue.
Deux symptômes selon la stratégie :
- Avec
attendre_navigation+naviguer:ERR_ABORTED at <url-cible>(Playwright annule la navigation car il est en mid-navigation SSE) - Avec
pause 1500+naviguer:TimeoutError 10000ms exceeded(la page SSE ne signale jamais “load complete”)
Dans les deux cas, url_au_moment_capture = ?vue=login — la page SSE a probablement invalidé ou consommé la session PHP pendant l’attente.
Contexte technique : ?action=cloner → PHP lance en background (nohup &) → redirige vers ?vue=log&... qui stream SSE. La page SSE ne se ferme jamais (streaming infini). page.goto() attend l’événement load qui n’arrive pas.
Impact : la documentation FN17 de Sillage indiquait comme contournement : « immédiatement après le clic de submit, enchaîner un naviguer vers une page stable ». Ce contournement est incorrect — naviguer échoue aussi.
Solution réelle validée : terminer le scénario immédiatement après l’evaluer click(). Diwall retourne succes: true avec url_finale = page d’avant le submit (le JS click est exécuté mais Playwright n’a pas encore tracké la navigation). Valider la mutation via SSH.
Exemple de scénario valide :
{"type": "evaluer", "script": "document.querySelector('[data-sillage=\"btn-confirmer-cloner-domaine\"]').click(); 'soumis'", "attendu": "soumis"}
→ Le scénario se termine ici. succes: true. Vérifier le clone via SSH.
Rappel FN8 : même si Diwall retourne succes: false sur une étape suivante, la mutation serveur est déjà partie (le evaluer click() a exécuté le JS immédiatement).
Version : Diwall v1.14.0 / rpa.py. Non testé sur les versions antérieures.
Session recherche commerciale multi-sites — 27 juin 2026 — FR-77 : WAF bloquent 39 % des sites e-commerce
Contexte : utilisation de Diwall pour une recherche d’achat en ligne sur des sites francophones de commerce (consoles de jeu reconditionnées, budget ≤ 200 €). 23 sites ciblés. Opérateur : Qwen (via OpenCode). Machine : la machine de développement. Version : Diwall v1.14.0.
FR-77 — Blocage WAF systématique sur les grands sites e-commerce
Description : la majorité des sites de grande distribution et des plateformes de reconditionnement bloquent Playwright via WAF (Cloudflare, CloudFront ou WAF propriétaire) avant même que le contenu soit chargé.
Statistiques observées sur 23 sites :
| Résultat | Nb sites | Pourcentage |
|---|---|---|
| Bloqués 403 (WAF) | 9 | 39 % |
| Timeout / pas de réponse HTTP | 6 | 26 % |
| URL invalide 404 | 5 | 22 % |
| Accessibles (HTTP 200 + contenu) | 2 | 8,7 % |
Sites accessibles : 2 sites SSR sans WAF. Sites bloqués : grandes enseignes et marketplaces e-commerce (9 sites).
Cause racine : Playwright expose navigator.webdriver = true par défaut. Les WAF modernes détectent ce signal et bloquent sans inspecter l’intention derrière la requête. Diwall ne dissimule pas ce signal — c’est un choix de transparence, pas une contrainte.
Impact : pour les cas d’usage de recherche commerciale sur des sites protégés par WAF, Diwall ne peut pas accéder au contenu. Ce n’est pas un bug Diwall — c’est une friction du paysage web actuel.
Ce qui fonctionne : sites SSR sans protection WAF, instances locales (SearXNG), applications internes sur réseau privé.
Ce qui ne fonctionne pas : grandes enseignes e-commerce et marketplaces protégées par WAF.
Non-solution : retenter la requête, changer le User-Agent manuellement, ajouter des délais — ces approches ne contournent pas Cloudflare Bot Management.
Recommandation documentée dans GUIDE_LLM.md (v1.14.1) : si un site retourne 403 immédiatement en --mode fast, il est WAF-bloqué. Ne pas retenter. Utiliser SearXNG pour la découverte, puis visiter manuellement les sites bloqués.
Backlog v1.15.x (requiert PHASE_PLANIFICATION) : stealth mode (playwright-stealth), distinction timeout_network / timeout_dom, persistance session inter-appels. Voir 10_ROADMAP.md.
Version : Diwall v1.14.0 / shot.py.
FR-78 — Droit à la navigation et contrat éthique de la Navigation Respectueuse
Note de renommage (v1.22.0) : ce relevé date de la v1.15.0, où la doctrine s’appelait « Navigation Citoyenne » et la clé de sortie
citoyennete. Le nom a changé — pas la doctrine. Le texte ci-dessous porte les noms en vigueur pour rester exact à la lecture ; le fait historique, lui, est inchangé.
Description : l’expérience de recherche commerciale multi-sites (FR-77) a déclenché une réflexion de fond sur la légitimité de navigation d’un LLM. Elle a abouti à la formulation du contrat éthique de la Navigation Respectueuse, inscrit dans la v1.15.0.
Constat : un LLM naviguant pour un opérateur humain depuis son IP, avec son autorisation explicite, est refusé à l’entrée de sites publics par détection automatique de navigateur headless. Ce refus ne porte pas sur l’intention — il ne peut pas l’évaluer. C’est une discrimination technique a priori.
La discrimination réelle : interdire à un LLM de naviguer parce qu’il est un LLM — et non parce qu’il se comporte de façon abusive. Un humain utilisant un lecteur d’écran n’est pas interdit de site web pour autant.
Le contrat éthique — deux volets inséparables :
Droit à l’accès : Claude navigue pour l’opérateur, depuis son IP, avec son autorisation. Identité déclarée, usage transparent. Si un mauvais usage est constaté, que l’opérateur en soit tenu responsable — pas condamné par principe.
Devoir de comportement : pour revendiquer ce droit, Diwall s’engage à naviguer de façon mesurée et respectueuse des ressources des sites visités :
- délai minimum entre les actions (
min_action_delay_ms) - plafond de pages et d’actions par run (
max_pages_par_run,max_actions_par_run) - métriques de son propre impact dans la boussole (
respect)
- délai minimum entre les actions (
Ce que Diwall ne fait pas : créer de fausse identité, cacher l’opérateur ou son IP,
prétendre être “Paul sur Safari”. Le mode furtif (--stealth, v1.15.0) retire les
marqueurs techniques automatiques — pas l’identité réelle.
Doctrine inscrite : _CADRE/SPECIFICATIONS/LEGITIMITE_ETRE_LLM.md (privé) et
manifeste public sur __DOMAINE_OPERATEUR__ section Philosophie.
Version : Diwall v1.15.0 (planifié). Session 43 — 30 juin 2026.
FR-79 — --stealth cassé depuis sa livraison (rupture d’API playwright-stealth 2.x)
Description : en tentant de mesurer l’impact de --stealth (item F de la v1.16.0,
benchmark post-consolidation), découverte que --stealth n’a jamais fonctionné en
production depuis son introduction en v1.15.0. Le paquet playwright-stealth
installé (2.0.3, conforme à requirements.txt >=2.0) a changé d’API entre la 1.x et la
2.x : la fonction stealth_sync(page) importée par shot.py n’existe plus. L’import
échouait silencieusement (except ImportError), --stealth se dégradait en no-op, et
navigator.webdriver restait exposé malgré le flag actif.
Défaut latent aggravant : la boussole affichait stealth_actif: true sur la seule
base du flag CLI demandé, pas de son application réelle — l’agent croyait naviguer
furtivement alors qu’aucune protection n’était appliquée.
Correctif (v1.16.0) : from playwright_stealth import Stealth +
Stealth().apply_stealth_sync(page) remplace l’ancien appel. stealth_actif dans la
boussole ne reflète désormais que le succès réel de l’application (variable
stealth_applique), plus la seule présence du flag.
Mesure post-correctif — bot.sannysoft.com (benchmark scenarios/test_stealth.json) :
| Signal | Sans --stealth | Avec --stealth |
|---|---|---|
navigator.webdriver | true | false |
navigator.plugins.length | 0 | 3 |
navigator.languages.length | 1 | 2 |
Tests d’empreinte échoués (td.failed) | 12 | 0 |
Tests d’empreinte réussis (td.passed) | 18 | 31 |
--stealth, une fois réellement appliqué, élimine la totalité des 12 détections
basiques du benchmark. Confirme la limite déjà documentée dans GUIDE_LLM.md :
ce résultat couvre le fingerprinting JS/navigateur — pas le TLS (JA3/JA4) ni
l’analyse comportementale Cloudflare Enterprise.
Panel FR-77 (23 sites) non reproductible à l’identique : la liste des 23 URLs
testées le 27 juin 2026 n’a été consignée nulle part (seules les statistiques
agrégées le sont). Reconstruire un panel de sites marchands réels pour une double
frappe (avec/sans stealth) sans intention d’achat réelle changerait la nature de la
démarche — proche d’un test de charge sur infrastructure commerciale tierce plutôt
que d’une navigation citoyenne légitime. Décision : ne pas engager ce test
autonomement ; le benchmark test_stealth.json (sites conçus pour être sondés)
sert de mesure de substitution pour la porte de décision v1.16.0/v1.17.0.
Effet sur la porte de décision v1.17.0 (JA3/JA4, Kimi K3) : partiellement informatif seulement. Le fingerprinting JS de base est désormais couvert — les 39 % de blocage 403 observés en FR-77 relevaient donc probablement d’un niveau de détection plus profond (TLS, comportemental), non mesurable par ce substitut. La question reste ouverte ; un nouveau panel commercial avec intention d’usage réelle est le seul moyen de trancher honnêtement.
Version : Diwall v1.16.0. Session 47 — 2 juillet 2026.
FR-80 — rpa.py --checkpoint n’a pas repris un long scénario multi-pages
Description : scénario de 113 actions (login + 36 navigations vers des fiches
« Gérer » de clones Sillage, un audit exhaustif demandé par l’opérateur) lancé avec
--checkpoint /tmp/gerer.checkpoint.json. Premier appel : succes: true,
plafond_atteint: max_actions_par_run, seulement 8 des 36 pages visitées (login +
7 navigations avant plafond). Le fichier de checkpoint attendu pour la reprise
n’existait plus après ce premier run — seul un *.session.json subsistait — comme
si le scénario avait été considéré terminé, alors qu’il restait 28 pages non
visitées. Une boucle appelant rpa.py --checkpoint une seconde fois s’est donc
arrêtée immédiatement, faute de fichier de checkpoint à reprendre.
Contournement appliqué : découpage manuel du scénario en 6 fichiers indépendants
de ~7 URLs chacun, chacun avec son propre login complet (remplir/cliquer sur le
formulaire), exécutés séquentiellement sans --checkpoint. Fonctionne, mais chaque
batch repaie le coût d’un login complet (5 actions) au lieu de réutiliser une
session — surconsommation d’actions/pages face au plafond de citoyenneté que
--checkpoint est censé précisément amortir sur un run long.
Non investigué ce soir (budget de session déjà consommé sur le diagnostic
métier) : si le bug vient de la condition de fin de checkpoint (peut-être un
mauvais calcul du nombre d’actions restantes, ou une confusion entre « plafond de
citoyenneté atteint » et « scénario terminé »), ou d’une interaction spécifique avec
--secrets + --reprendre-session implicite. Reproductible avec n’importe quel
scénario JSON dépassant ~8-9 navigations pleine-page et --checkpoint sur le même
fichier de sortie.
Version : Diwall v1.17.0. Session Sillage — 2/07 au 3/07/2026.
FR-81 — force: true sur cliquer insuffisant pour un checkbox dans un <dialog> ouvert via showModal() JS
Description : dialogue de confirmation de suppression (<dialog id="dialog-supp-X">)
ouvert via evaluer (document.getElementById(...).showModal()), puis tentative de cocher
la case de confirmation et de cliquer le bouton de soumission via cliquer avec
"force": true, conformément à GUIDE_LLM_INTERACTIONS.md (« Element is in DOM but
CSS-hidden or behind showModal() → cliquer avec force: true »). Échec systématique :
Locator.click: Element is not visible — comportement inattendu, force: true est censé
justement bypasser ce contrôle.
Cause probable : l’échec précédent (avant l’ajout de force) venait en réalité d’un
sélecteur CSS mal formé (#form-supp-example.fr-confirm — le point dans l’ID de domaine
est interprété comme un sélecteur de classe CSS chaîné, #form-supp-example + .fr-confirm,
qui ne correspond à rien). Une fois le sélecteur corrigé (\\. échappé en JSON), l’échec a
persisté avec force: true, cette fois avec le bon élément résolu (confirmé par le message
d’erreur citant le bon outerHTML) — donc force seul n’a pas suffi à rendre l’élément
cliquable dans ce contexte <dialog> précis.
Contournement appliqué : abandon de cliquer/force pour cette famille d’éléments,
bascule complète sur evaluer JS (showModal() + checkbox.checked = true +
dispatchEvent(change) + submitButton.click() dans un seul script). Fonctionne à 100 %
sur 4 répétitions (déconnexion de 4 domaines successifs, même dialogue).
Non investigué ce soir : si force: true échoue spécifiquement sur les éléments
enfants d’un <dialog> ouvert par script (par opposition à un <dialog> ouvert par un
vrai clic utilisateur), ou si c’est un problème plus général de timing entre showModal()
et la disponibilité du layout au moment du clic suivant dans la même action list.
Reproductible avec le dialogue de suppression de domaine Sillage
(?vue=reglages&client=..., section Domaines, bouton « Supprimer » sur une carte domaine).
Confirmation du même pattern (12/07/2026, Diwall v1.20.0) : reproduit à l’identique sur
un élément non lié à un <dialog> — un toggle switch CSS (case à cocher masquée visuellement,
<label> custom par-dessus) pour activer la réplication automatique. cliquer avec
force: true échouait de la même façon (« element outside of viewport » / not visible).
Même contournement, même succès : evaluer JS direct (checkbox.checked = true +
dispatchEvent('change')). Élargit la portée de FR-81 : le problème n’est pas spécifique à
showModal(), mais à toute la famille des éléments interactifs masqués visuellement au
profit d’un habillage CSS custom (dialog, toggle switch, probablement aussi les selects
stylés). Renforce la recommandation d’escalade déjà formulée : documenter evaluer JS
comme option par défaut, pas seulement de secours, pour toute cette famille.
FR-82 — boussole.session_derive a orienté un diagnostic vers une fausse piste (session expirée) alors que la cause réelle était une erreur HTTP 500 applicative masquée
Description : diagnostic d’un bouton d’action délégué (« Sauvegarder maintenant »,
espace client L2 de Sillage) qui redirigeait systématiquement vers la page de login au lieu
d’exécuter l’action. Le signal boussole.session_derive de Diwall a orienté l’investigation
vers l’hypothèse « session L2 expirée / cookie perdu entre deux étapes » — plusieurs cycles
de test (--reprendre-session/--sauver-session, vérification document.cookie, reproduction
en un seul appel Playwright continu pour éliminer une perte de cookie inter-process) ont été
dépensés sur cette piste avant qu’elle ne soit écartée.
Cause réelle (trouvée hors Diwall, par curl brut reproduisant le POST exact) : un bug
applicatif Sillage à deux étages — un formulaire HTML avec action="?" qui effaçait un
paramètre GET nécessaire à l’authentification côté serveur, puis (une fois ce premier bug
corrigé) une fonction PHP non chargée provoquant une erreur HTTP 500 masquée en page blanche
côté client (display_errors=0, comportement de production correct mais qui cache le signal).
Le symptôme observable (redirection vers login) était identique à une vraie expiration de
session — mais la cause n’avait rien à voir avec la session elle-même.
Ce qui a permis de sortir de la fausse piste : abandon de Diwall pour cette étape précise,
curl manuel reproduisant le POST exact — la distinction HTTP 500 vs HTTP 302 (redirection
d’auth) a immédiatement écarté l’hypothèse session.
Non investigué ce soir : si session_derive se base sur un pattern générique
(« retour inattendu vers une page de login ») sans distinguer le code HTTP réellement reçu, ou
sur un autre signal. Si c’est le cas, le signal pourrait être affiné pour distinguer un vrai
302 d’authentification d’un flux qui ressemble à une perte de session vue depuis le DOM final
(page de login affichée) mais qui provient d’une erreur serveur en amont. Suggestion : exposer
le code HTTP de la dernière réponse à côté du signal session_derive, pour que l’appelant
puisse écarter rapidement l’hypothèse session si le code n’est pas 302/401/403.
FR-83 — --sauver-session/--reprendre-session ne préserve pas l’état DOM transitoire (dialog ouvert, <select> positionné) entre deux invocations séparées
Description : test d’un formulaire dans un <dialog> (sélection source/destination
pour une action de duplication, Sillage). Séquence en plusieurs invocations séparées de
shot.py --reprendre-session : (1) clic pour ouvrir le dialog, (2)/(3) positionnement de
deux <select> via evaluer + dispatchEvent(change), vérifié correct par lecture DOM
immédiate, (4) clic sur le bouton de soumission dans une quatrième invocation séparée.
Le clic a réussi (succes: true) mais a soumis les valeurs par défaut du HTML, pas
celles positionnées à l’étape (2)/(3) — le dispatcher a reçu l’inverse de ce qui était
attendu (source/dest inversés).
Cause : --sauver-session/--reprendre-session restaure l’état de la session HTTP
(cookies), mais chaque invocation recharge la page depuis zéro — un <dialog> ouvert
par un clic précédent et des valeurs de <select> positionnées par evaluer sans
soumission ne survivent pas au rechargement. Le clic de l’étape (4) a donc cliqué un bouton
de soumission bien réel mais à l’intérieur d’un dialog rouvert avec ses valeurs HTML par
défaut, sans que rien ne le signale (succes: true, aucune erreur).
Contournement appliqué : regrouper ouverture + positionnement + vérification +
soumission dans une seule invocation avec --actions (liste d’actions), au lieu de
--action unique répété sur plusieurs appels --reprendre-session. Fonctionne à 100 % une
fois regroupé — reproductible et vérifié par lecture DOM juste avant la soumission, dans le
même appel.
Conséquence potentiellement grave évitée de justesse : dans ce cas précis, l’inversion
source/destination aurait pu écraser un domaine différent de celui visé si les deux
domaines n’avaient pas eu un contenu déjà proche l’un de l’autre au moment du test — un
appelant moins prudent (pas de vérification de l’état avant/après) aurait pu attribuer à
tort le succès rapporté par Diwall (succes: true) à l’opération réellement voulue.
Suggestion d’escalade pour GUIDE_LLM_INTERACTIONS.md : documenter explicitement que
tout état DOM non soumis (dialog ouvert, champ rempli, select changé) est perdu entre deux
invocations séparées de --reprendre-session, même si la session HTTP reste valide — une
séquence « ouvrir → remplir → soumettre » doit toujours être un seul appel --actions,
jamais fragmentée en plusieurs --reprendre-session successifs quand une soumission de
formulaire est en jeu.
Version : Diwall v1.20.0. Session Sillage — 12/07/2026.
Escalade utile pour GUIDE_LLM_INTERACTIONS.md : la note actuelle sur force: true
et showModal() (ligne ~31, decision tree item 4) mériterait un avertissement — dans un
<dialog> ouvert par script plutôt que par interaction utilisateur, préférer d’emblée
evaluer JS pour toute la séquence plutôt que cliquer + force.
Version : Diwall v1.17.2. Session Sillage — 03/07/2026.
Session HOST_VPS — 13 juillet 2026 — Construction d’un dashboard Grafana (édition SPA, éditeur Monaco)
Première session de ce type sur le projet HOST_VPS : piloter Grafana via son interface web (édition de panneau, requêtes PromQL) plutôt qu’une simple validation de login. Plusieurs frictions inédites, propres à une SPA d’édition complexe.
Confirmation de FR-83 dans un contexte différent (dashboard non sauvegardé)
Même cause, autre manifestation : construire un panneau Grafana étape par
étape avec un appel shot.py par étape et --sauver-session/
--reprendre-session entre chaque renvoyait systématiquement à l’écran
« New dashboard » vierge — le panneau en cours d’édition n’existe que dans le
DOM/state React de la session navigateur précise, jamais sauvegardé nulle
part ailleurs. Même remède que FR-83 : tout le parcours (login compris) dans
une seule invocation tant que l’objet n’est pas persisté côté serveur. Une
fois le dashboard réellement sauvegardé (UID obtenu), reprendre une session
pour la suite (ajouter un panneau à un dashboard existant) redevient fiable.
Un détail nouveau à ajouter à la règle de FR-83 : la frontière n’est pas
« formulaire vs pas formulaire » mais persisté vs non persisté — un
dashboard non sauvegardé est un cas exactement aussi volatile qu’un dialog
non soumis.
Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.
FR-84 — cliquer_som avec un ID capturé avant une mutation DOM non-navigationnelle peut viser le mauvais élément
Description : dans l’éditeur de requête Grafana, un bouton bascule « Builder » / « Code » fait disparaître un toggle « Explain » situé juste avant dans le DOM. Un ID de bouton (« Run queries ») capturé avant ce basculement ne correspondait plus au bon élément une fois en mode Code (décalage d’un cran). Une tentative d’enchaîner plusieurs actions à la suite sans revérifier le SoM après le basculement a produit une fois un échec franc (« élément non trouvé ») et, plus inquiétant, une fois un comportement inattendu : retour sur l’écran de login après 13 actions apparemment réussies, sans message d’erreur avant l’échec final sur un ID suivant. Cause exacte non confirmée (peut-être un clic ayant atteint un élément de déconnexion par pur hasard de décalage d’ID) — non investigué davantage, pas reproduit une seconde fois après correction.
Contournement appliqué : ne jamais réutiliser un ID de SoM au-delà d’une action qui modifie potentiellement le DOM, même sans navigation complète — un simple toggle suffit. Recapturer le SoM immédiatement après toute mutation, avant de s’en servir pour cibler l’action suivante.
Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.
FR-85 — Éditeur Monaco (role="code") : remplir_som sur l’ID visible échoue silencieusement, remplir par sélecteur générique fait un strict-mode violation
Description : le champ de requête PromQL de Grafana (mode “Code”) est un
éditeur Monaco (VS Code), rendu comme <div role="code" class="monaco-editor...">
avec un <textarea> caché à l’intérieur pour la saisie clavier réelle. Le
SoM capturait cet élément comme un BUTTON au texte vide et non interactif
au sens standard — remplir_som dessus ne fonctionnait pas. Tentative
suivante avec cliquer sur .monaco-editor : échec strict-mode (2 éléments
correspondent — l’éditeur réel et un widget de renommage Monaco caché
ailleurs sur la page, tous deux avec la classe monaco-editor).
Contournement appliqué : reconnaissance ciblée via evaluer
(document.querySelector('[role="code"] textarea')) pour confirmer le
sélecteur précis, puis remplir (pas remplir_som) directement sur
[role="code"] textarea — plus spécifique que .monaco-editor textarea,
lève l’ambiguïté. Fonctionne du premier coup une fois le bon sélecteur
identifié ; le texte apparaît avec la coloration syntaxique normale,
confirmant qu’il passe bien par le modèle interne de Monaco et pas par un
simple value= cosmétique.
Suggestion pour GUIDE_LLM_INTERACTIONS.md : ajouter un cas « éditeurs
de code embarqués (Monaco, CodeMirror) » — SoM les numérote souvent mal (pas
un vrai <input>/<textarea> visible), et [role="code"] textarea (ou
équivalent CodeMirror .cm-content) est plus fiable qu’un sélecteur de
classe générique, sujet à collision avec des instances cachées.
Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.
FR-86 — Aucune prise en charge native du challenge HTTP Basic Auth (401)
RÉSOLU en v1.21.0 (15/07/2026).
--http-credentialsrésout désormais les identifiants HTTP Basic depuis le vault et les injecte au contexte navigateur, scopés à l’origine de la cible. Validé contre une cible Basic Auth réelle (pas seulement une fixture locale) : succès dès le premier essai. Détail complet :docs/GUIDE_LLM_SESSIONS.mdsection--http-credentials,docs/JOURNAL.mdsession 55.
Description (constat d’origine, avant résolution) : deux interfaces
d’administration auto-hébergées (un tableau de bord d’observabilité et un
service de métriques associé) étaient protégées par un basic_auth Caddy
(couche réseau, avant même l’application). shot.py --help passé en revue
intégralement : aucune option pour fournir des identifiants HTTP Basic (pas
de --http-credentials, pas de verbe d’action dédié). Le mécanisme de
vault (--secrets, remplir_som + vault_cle) est conçu pour des
formulaires web, pas pour un challenge navigateur natif qui bloque la page
avant même son rendu.
Contournement appliqué : tunnel SSH local (ssh -L <port>:127.0.0.1:<port> __HOST_VPS__) vers les ports internes des applications, pour les valider
directement sans jamais faire transiter le mot de passe basicauth par une
URL en clair. Fonctionne bien pour un service interne joignable en SSH, mais
n’aide en rien si la cible n’est que publique (pas de SSH possible vers
le backend).
Évolution livrée (v1.21.0) : --http-credentials résout les
identifiants depuis le vault (même discipline que remplir_som +
vault_cle, jamais en clair dans les arguments CLI) et les injecte à la
création du contexte navigateur (browser.new_context(http_credentials=…)),
scopés à l’origine de la cible. Revalidé contre les deux interfaces
d’origine de ce constat — succès dès le premier essai avec le mode par
défaut le plus sûr (send: "unauthorized").
Version : Diwall v1.20.0 (constat). Résolu en v1.21.0. Session
__HOST_VPS__ — 13-15/07/2026.
Ce qui a bien fonctionné, à noter aussi
- Une fois la discipline “reconnaissance avant mutation” réellement suivie
(capturer le SoM, l’inspecter, agir, plutôt qu’enchaîner à l’aveugle), zéro
nouvel échec sur le reste du parcours — y compris la pose du titre du
panneau via manipulation directe du setter React natif (
evaluer+Object.getOwnPropertyDescriptor(...).set), nécessaire car ce champ ignore uninput/changeevent synthétique simple sans passer par le setter natif deHTMLInputElement. - La preuve visuelle (capture) a été le vrai livrable utile face à
l’opérateur : un
curlà 200 n’aurait rien dit sur si Grafana affichait réellement un dashboard fonctionnel avec de vraies données après la coupure de l’OAuth Plesk mort.
Version : Diwall v1.20.0. Session HOST_VPS — 13/07/2026.