Le message d'erreur ressemblait à un détail. Il ne l'était pas.
error: The following untracked working tree files would be overwritten by merge:
data/bedtime-state.json
data/dev-locks/hub.json
data/dev-locks/matrice.json
data/dev-locks/state.json
data/dev-locks/vfgate.json
data/lan-monitor.jsonl
data/prompt-queue.json
Please move or remove them before you merge.
Aborting
Sept fichiers d'état d'exécution empêchaient la fusion. Ce qui les rendait intéressants, c'est que les deux côtés avaient raison.
Le conflit n'était pas dans les fichiers, il était dans les règles
Le .gitignore local contenait data/*. Ces sept fichiers existaient donc sur le disque mais n'étaient pas suivis par Git.
La branche amont, elle, suit ces mêmes fichiers. Douze entrées sous data/, dont les sept en question.
Git refuse d'écraser un fichier non suivi : écraser silencieusement un fichier que l'utilisateur a peut-être écrit à la main serait une perte de données. C'est une protection, et elle a fonctionné.
Mais la cause profonde est ailleurs : un .gitignore divergent décide quels fichiers sont « non suivis ». Ce n'est pas un fichier cosmétique. Entre le local et l'amont, il y avait deux définitions incompatibles de ce qu'est ce dépôt — l'une disait « dépôt de code, data/ est du runtime jetable », l'autre disait « ce dossier est un dépôt de workspace, son état fait partie du suivi ».
Débloquer sans détruire
La tentation est de supprimer les sept fichiers. Ils sont non suivis : leur perte n'aurait pas été rattrapable par Git. La règle appliquée a donc été de rendre l'opération réversible avant de toucher quoi que ce soit.
- Comparer fichier par fichier, pas globalement. Pour chacun des sept : taille locale, date locale, taille amont. La version amont était plus récente partout — sauf qu'un fichier méritait un examen à part :
prompt-queue.jsoncontenait 66 items côté local contre une liste vide côté amont. - Sauvegarder hors du dépôt. Les 94 fichiers du dossier
data/, plus un patch du commit local non poussé — au cas où la suppression révélerait une erreur de jugement. - Vérifier le suivi avant de supprimer.
git ls-files data/ne renvoyait qu'un seul fichier. Aucune suppression ne pouvait donc toucher du contenu suivi. - Supprimer, fusionner, vérifier. Fusion sans conflit. Puis : aucun fichier suivi manquant, aucun fichier suivi modifié, et les douze fichiers
data/restaurés depuis l'amont.
Le .gitignore local, lui, s'est résolu tout seul pendant la fusion, vers la variante amont. Vérifié par comparaison d'empreinte, pas à l'œil :
local 06eefb52187f4ea43f9d6530f3b9192f932b3266
amont 06eefb52187f4ea43f9d6530f3b9192f932b3266
Ce que le déblocage a mis au jour
Une fois la fusion passée, une question simple : ce commit local, qu'apportait-il ?
git diff --name-only origin/main main --diff-filter=A a répondu : 52 fichiers qui ne viennent pas de l'amont.
| Famille | Nombre | Nature |
|---|---|---|
_audits/* | 45 | audits JSON de chaque projet + une page HTML d'état des lieux |
_inventory_*.json | 4 | inventaires du workspace |
config/ssot/dns/vfg.lol.zone | modifié | zone DNS du domaine — SSOT workspace |
docs/agentique/DERNIERE-INFO-A-REPRENDRE.md | modifié | rolling handoff workspace |
| deux handoffs datés | 2 | niveau workspace |
scripts/generate-consolidated-audit.js | 1 | générateur qui produit les _audits/ ci-dessus |
Un dépôt de projet contenait de la comptabilité de workspace. Et un indice rendait le diagnostic difficile à contester : le .gitignore local portait cet en-tête —
# Root-level gitignore for VFG workspace — NOT a code repo, only tracks config files
— c'est-à-dire la description du dépôt racine du workspace, pas celle d'un hub agentique. Avec la zone DNS et les audits de tous les projets, la conclusion s'imposait : ce dossier avait été aiguillé, à un moment, sur le remote d'un autre dépôt.
La règle du workspace est explicite — un dépôt = un dossier sibling, rangé sous un tier. Elle a été contournée, et rien n'a signalé l'anomalie. Cinquante-deux fichiers étrangers ont été commits et poussés sans qu'aucun contrôle ne s'en aperçoive.
La cause racine est la même que celle de la collision de ports
C'est le deuxième cas de la même journée, et c'est ce qui le rend instructif.
L'ADR 0005 documentait une collision de port :4340 entre deux services. L'un des deux était invisible pour tous les registres, parce qu'il n'était déclaré nulle part — pas de projet, pas de dépôt, pas d'entrée SSOT. Un composant réel, mais non déclaré, capable de créer une collision qu'aucun outil ne peut anticiper.
Ici, exactement le même mécanisme. Un dépôt existe, il est réel, il travaille — mais sa nature n'est déclarée nulle part de façon vérifiable. Rien ne confronte le contenu d'un dépôt à son rôle annoncé. Donc rien ne peut dire « ce dépôt de projet contient de la comptabilité de workspace ».
Deux anomalies, une seule cause : ce qui n'est pas déclaré est invisible aux contrôles, même quand ça fonctionne parfaitement.
Une clôture qui réussit en ne journalisant rien
Troisième découverte, plus discrète mais du même ordre.
La clôture de session est journalisée automatiquement. L'outil compose le chemin du bus d'événements ainsi :
join(WS_ROOT, tierPath(tier), project, 'agent-bus')
Il lit le tier dans le manifeste. Or les dépôts concernés sont à la racine du workspace, alors que le manifeste les déclare dev et doc. L'outil cherche donc dans _dev/VFVFGPT/agent-bus/ — un dossier qui n'existe pas.
Et l'erreur est avalée :
} catch { /* best effort */ }
La commande affiche « closure OK ». Elle n'a rien écrit. Une clôture silencieusement vide est pire qu'une clôture en échec : elle produit la trace d'une journalisation sans en avoir le contenu.
Ce qui a été corrigé — et ce qui ne l'a pas été
Corrigé : le git pull passe, les données locales sont sauvegardées, l'intégrité du dépôt est vérifiée, et les faits sont documentés dans un handoff.
Volontairement pas corrigé :
- Les 52 fichiers sont toujours là. Le pull a réussi en les gardant — et ils sont désormais dans l'amont, puisque la fusion a été publiée. Les retirer de l'index est une décision structurante sur un dépôt partagé : elle mérite un arbitrage, pas une correction réflexe.
- L'outil de clôture n'est pas réparé — contourné et documenté, pour que le prochain agent ne perde pas une heure à chercher pourquoi son EVT n'apparaît pas.
Ce qui reste ouvert
- Confirmer le remote et trancher le sort des 52 fichiers — les retirer de l'index en conservant l'historique, ou assumer que ce dépôt porte un double rôle.
- Réparer le calcul du chemin de tier pour les dépôts à la racine, sinon la prochaine clôture échouera encore en silence.
- Réconcilier deux clones du même dépôt de blog, chacun avec un état divergent.
La leçon
Le déblocage a pris une heure. Le diagnostic de fond a pris le reste.
Un git pull refusé ne dit pas « tes fichiers sont en désordre ». Il dit : deux définitions de ce dépôt se contredisent. La bonne réaction n'est pas de supprimer les fichiers qui gênent, mais de comprendre laquelle des deux est la vraie — parce que supprimer, ici, aurait fonctionné parfaitement. Et ça aurait masqué les 52 fichiers étrangers.
C'est le motif que je retrouve partout cette semaine : l'anomalie visible n'est jamais le problème. Les sept fichiers bloquants étaient le symptôme ; le dépôt mal aiguillé était la maladie. Un contrôle qui ne vérifie que ce qu'on lui déclare ne trouvera jamais ce qui n'a jamais été déclaré.