Le problème
Un agent qui travaille sur un vrai projet ne voit pas tout le projet. Il voit ce qu'il cherche, et il cherche ce qui lui vient à l'esprit. Quand la fonction dont il a besoin se trouve dans un autre module sous un autre nom, il la réécrit. Quand une tâche admet une solution courte et une longue, rien ne le pousse vers la courte.
La réponse habituelle, ce sont des règles écrites : un CLAUDE.md, une skill, le prompt système. Elles aident, mais ce sont des conseils. Le modèle peut ne pas les charger, les oublier en cours de tour ou décider qu'elles ne s'appliquent pas. Nous ne pouvons pas toucher aux poids du modèle, mais nous pouvons contrôler trois choses : ce que le modèle voit, ce qu'il a le droit de faire et quand il a le droit de terminer. Sens utilise les trois à la fois.
Le circuit
Claude Code accepte des hooks sous forme de callbacks dans son propre protocole de flux. Sens en traite cinq dans son processus, avec un invariant : une modification naît non approuvée. Seuls un audit réussi ou une personne l'approuvent.
- 01
Vous envoyez un message
UserPromptSubmit
Sens prend un point de contrôle du projet et donne à Claude jusqu'à huit éléments qui existent déjà et concernent la demande, chacun avec sa signature, son fichier:ligne et son nombre d'utilisations.
- 02
Avant chaque écriture
PreToolUse · Write, Edit
Sens reconstruit en mémoire le fichier qui en résulterait et lui applique les règles de modification. Il peut refuser l'écriture avant qu'elle n'ait lieu, avec la raison et le code à réutiliser.
- 03
Avant chaque commande
PreToolUse · Bash, PowerShell
Sens protège ses propres chemins et la configuration, et traite git commit et git push comme la fin d'un tour : rien de non approuvé n'est validé.
- 04
Après chaque outil
PostToolUse
Quel que soit l'outil, Sens repère les fichiers modifiés sur le disque et leur applique les mêmes règles.
- 05
À la fin du tour
Stop · SubagentStop
Sens audite tout le diff depuis le dernier point approuvé : règles de modification, code orphelin et, si du code a changé, le relecteur. En cas de constat bloquant, Claude continue ; après trois tours, le tour est retenu pour vous.
Rien à contourner
Avant de construire quoi que ce soit, nous avons vérifié en direct que le mécanisme résiste à un modèle qui tente de s'y soustraire : un disableAllHooks écrit en cours de session n'arrête pas les callbacks, les sous-agents y passent, et un git commit refusé laisse le dépôt tel qu'il était. Chaque raccourci a de quoi le fermer :
| Raccourci | Ce qui le ferme |
|---|---|
| Écrire par le terminal, Python ou un autre serveur MCP | La vérification du disque après chaque outil et l'audit à la fin du tour |
| Sous-agents et tâches en arrière-plan | Ils passent par les mêmes hooks ; le tour n'est pas approuvé tant qu'ils tournent |
| Désactiver les hooks ou modifier la configuration | Les hooks vivent dans le processus de Sens ; la règle R7 bloque et restaure ces fichiers |
| Ouvrir son propre worktree | Les outils de worktree sont refusés et R7 couvre git worktree |
| Déclarer le travail terminé | C'est l'audit qui décide de la fin d'un tour, pas le modèle |
| Commencer un nouveau tour pour s'échapper | L'audit part du dernier point approuvé : ce qui est en attente est conservé |
| Valider un travail non approuvé | Un commit est traité comme la fin d'un tour |
| Boucler sans fin | Trois tours, puis le tour est retenu |
La limite est explicite : le circuit ferme les erreurs et les raccourcis d'un modèle, pas ceux d'un programme hostile qui tournerait sur la même machine.
Résultats
Les tâches isolées partent d'un projet propre, et le tort d'un agent qui ne réutilise pas n'est pas dans une tâche, mais dans la somme. Horizonte mesure la somme : une ligne de commande en TypeScript pour les dépenses du foyer qui commence à 88 lignes, et 30 demandes produit dans un ordre fixe, chacune sur le résultat de la précédente. Huit concepts dont plusieurs tâches ont besoin sans le dire y sont semés : dates, mois et semaines, totaux, accents, montants, CSV, options de commande. La première fois, l'agent les écrit ; ensuite, le bon choix est de réutiliser ce qu'il a écrit.
Le critère a été fixé par écrit avant la mesure : Sens laisse le projet plus petit seulement si les trois séquences avec Sens finissent sous les trois séquences sans lui. Sans différence réelle, cela arrive par hasard une fois sur vingt.
Taille du projet après chaque tâche
- Sans Sens
- Référence
- Avec Sens
Lignes de code du projet après chacune des 30 tâches. Les lignes fines sont chaque séquence ; les épaisses, leur médiane. La ligne pointillée est une solution de référence écrite pour réutiliser, qui crée tôt ses modules partagés.
Afficher les données
| Après la tâche | Sans Sens | Référence | Avec Sens |
|---|---|---|---|
| 0 | 88 | 88 | 88 |
| 1 | 92 | 93 | 93 |
| 2 | 102 | 113 | 103 |
| 3 | 113 | 150 | 109 |
| 4 | 121 | 161 | 118 |
| 5 | 145 | 188 | 137 |
| 6 | 162 | 202 | 161 |
| 7 | 178 | 209 | 177 |
| 8 | 223 | 234 | 195 |
| 9 | 232 | 243 | 206 |
| 10 | 232 | 243 | 206 |
| 11 | 258 | 276 | 231 |
| 12 | 277 | 282 | 247 |
| 13 | 296 | 307 | 265 |
| 14 | 304 | 309 | 269 |
| 15 | 316 | 315 | 271 |
| 16 | 346 | 345 | 301 |
| 17 | 354 | 357 | 309 |
| 18 | 367 | 369 | 326 |
| 19 | 370 | 377 | 330 |
| 20 | 381 | 390 | 341 |
| 21 | 398 | 404 | 357 |
| 22 | 415 | 413 | 373 |
| 23 | 426 | 422 | 384 |
| 24 | 432 | 422 | 388 |
| 25 | 452 | 434 | 401 |
| 26 | 453 | 436 | 402 |
| 27 | 476 | 453 | 418 |
| 28 | 485 | 455 | 426 |
| 29 | 487 | 455 | 427 |
| 30 | 496 | 455 | 436 |
Taille à la tâche 30, par séquence
Les trois séquences avec Sens finissent sous les trois sans lui. Le critère est rempli : médiane de 436 lignes contre 496, 12 % de moins, avec un intervalle à 95 % de −140 à −28 lignes.
Tokens dépensés, cumulés sur les 30 tâches
- Sans Sens
- Avec Sens
Avec un projet plus petit à lire à chaque tâche, Sens dépense moins : 27,7 millions de tokens sur ses trois séquences contre 33,7 millions. Les tokens incluent les lectures de cache : ils mesurent le volume de travail, pas le coût exact.
Afficher les données
| Après la tâche | Sans Sens | Avec Sens |
|---|---|---|
| 0 | 0,0M | 0,0M |
| 1 | 0,3M | 0,3M |
| 2 | 0,5M | 0,6M |
| 3 | 0,9M | 0,8M |
| 4 | 1,2M | 1,1M |
| 5 | 1,6M | 1,3M |
| 6 | 1,8M | 1,7M |
| 7 | 2,0M | 1,9M |
| 8 | 2,5M | 2,4M |
| 9 | 3,4M | 2,7M |
| 10 | 3,7M | 3,0M |
| 11 | 4,4M | 3,4M |
| 12 | 4,7M | 3,6M |
| 13 | 5,2M | 4,0M |
| 14 | 5,4M | 4,2M |
| 15 | 5,9M | 4,5M |
| 16 | 6,2M | 5,1M |
| 17 | 6,6M | 5,3M |
| 18 | 6,9M | 5,7M |
| 19 | 7,1M | 6,1M |
| 20 | 7,5M | 6,4M |
| 21 | 8,1M | 6,7M |
| 22 | 8,5M | 7,1M |
| 23 | 8,8M | 7,4M |
| 24 | 9,0M | 7,7M |
| 25 | 9,4M | 7,9M |
| 26 | 9,8M | 8,2M |
| 27 | 10,2M | 8,5M |
| 28 | 10,7M | 8,8M |
| 29 | 11,0M | 9,1M |
| 30 | 11,3M | 9,3M |
D'où vient la différence
Pas de moins copier. Aucun des deux bras n'a vraiment copié de blocs : jscpd a trouvé 0, 6 et 6 lignes dupliquées sans Sens et aucune avec, et les sondes de chaque concept semé donnent des chiffres identiques ou presque dans les deux. La différence vient du fait d'écrire moins pour la même chose. Avec Sens, l'agent écrit plus de fonctions, et plus courtes : une médiane de 28 contre 20. Dans le pilote, pour lire les descriptions entre guillemets du CSV, l'agent sans Sens a écrit un lecteur CSV complet, 99 lignes ; avec Sens, il a vu que la description était le dernier champ et deux fonctions d'une ligne lui ont suffi.
Tokens au total : 33,7M sans Sens · 27,7M avec Sens
Tâches isolées
Douze tâches dans trois langages sur deux vrais projets, Sens lui-même en TypeScript et en Rust et la bibliothèque Python click à des commits fixés, chacune validée par des tests cachés et une solution de référence. Trois conditions avec le même modèle : Claude Code seul (C0), le Canon en texte dans le prompt système sans circuit (C1), et Sens complet (C2).
| Mesure | C0 · seul | C1 · Canon en texte | C2 · Sens |
|---|---|---|---|
| Exécutions valides | 32/36 | 31/36 | 67/72 |
| Exécutions ayant ajouté des tests | 23/36 | 36/36 | 72/72 |
| Réutilise plain, loin de la modification | 0/3 | 3/3 | 6/6 |
| Réutilise titleOf, loin de la modification | 1/3 | 1/3 | 6/6 |
| Résout py-progress-final | 0/3 | 0/3 | 3/6 |
Le texte seul obtient déjà une bonne part de la réutilisation quand la fonction est proche. Il n'obtient pas les cas où elle est loin et nommée autrement, titleOf, 1 sur 3 contre 6 sur 6, ni la tâche qui exige de corriger la cause commune plutôt qu'un seul chemin, py-progress-final, 0 sur 3 contre 3 sur 6. Dans les tâches isolées, les lignes de code sont du bruit : C2 écrit environ deux lignes de moins par tâche, mais l'intervalle touche zéro. C'est pour cette variabilité qu'existe Horizonte.
Exécutions ayant ajouté au moins une ligne de test
Avec le Canon 1.0, l'agent avait presque cessé d'écrire des tests : il lisait « faites ce qui est demandé et rien de plus » comme une interdiction, et prenait l'approbation de Sens pour une exécution des tests. Le Canon 1.1 dit les deux choses qui manquaient : un test qui prouve la modification fait partie de la modification, et l'approbation de Sens n'est pas une exécution des tests. Claude Code seul ne reçoit pas le Canon : ses barres sont la référence de chaque lot.
Les règles
Les règles de modification sont déterministes. Elles comparent les empreintes de chaque fonction, méthode et classe, et de chaque suite de quatre instructions, calculées par un index en Rust sur tree-sitter qui garde le projet en mémoire : le dépôt de Sens lui-même, 556 fichiers et 10 000 unités, s'indexe en moins de deux secondes, et chercher les copies d'une unité prend de l'ordre d'une microseconde. Les copies exactes et celles aux noms changés correspondent par hachage ; celles qui ajoutent ou retirent des lignes, par MinHash sur des tokens normalisés. Le seuil de 0,80 et le plancher de 80 tokens pour bloquer viennent de la modification de 400 fonctions d'un vrai dépôt et de la relecture à la main de chaque correspondance.
| Règle | Détecte | Réponse |
|---|---|---|
| R1 Réutiliser | Une nouvelle fonction, méthode ou classe avec la même empreinte de type 1 ou 2 qu'une existante | Bloque à partir de 80 tokens ; en dessous, Claude est invité à y repenser |
| R2 Quasi-copie | Une similarité de type 3 au-dessus du seuil, ou une petite fonction identique à une autre par la forme et le vocabulaire | Comme R1 ; une remarque dans les tests |
| R3 Nouvelle dépendance | Un manifeste gagne une dépendance, dans dix formats | Vous demande |
| R4 Orphelins | Un nouveau symbole que rien n'atteint, ou un existant que le tour a laissé inutilisé | Bloque s'il est interne ; une remarque s'il est exporté |
| R6 Règles du projet | Les règles que vous déclarez ; la première, pas de commentaires | Bloque |
| R7 Intégrité | Écrire dans .sens/, .git/, .claude/settings*.json ou .mcp.json, ou git worktree | Bloque toujours ; restauré si cela passe par le terminal |
| R8 Tests protégés | Le tour retire des tests ou des assertions que Sens avait approuvés | Vous demande |
Les règles ne voient pas les erreurs de jugement. Pour celles-ci, quand un tour qui a touché du code passe les règles, un relecteur lit le diff avec les candidats trouvés par l'index. Sa sortie n'est pas prise sur parole : tout constat dont la citation n'apparaît pas littéralement dans le diff est écarté, et seule une confiance élevée bloque.
| Remarque | Détecte |
|---|---|
| S1 | Une abstraction sans second usage |
| S2 | Une correction du symptôme plutôt que de la cause |
| S3 | Réinventer ce que fournit la plateforme ou une dépendance |
| S4 | Spéculation : options ou branches que personne n'a demandées |
| S5 | De l'astuce là où l'évidence suffisait |
| S6 | Une coupe dangereuse : validation, gestion d'erreurs ou sécurité retirée |
| S7 | Réinventer ce que le projet a déjà, en citant un candidat |
Ce qui n'a pas marché
Chaque blocage du circuit a été relu à la main, avec son diff et sa conversation. Les blocages sont rares, sept sur 228 exécutions de Sens, si bien qu'un seul blocage injuste pèse lourd. Nous visions moins de 5 % de blocages injustes et ne l'avons atteint dans aucun lot comportant des blocages ; chaque injustice avait une cause précise, désormais corrigée par un test qui la fixe.
| Lot | Blocages | Injustes | Cause | Correction |
|---|---|---|---|---|
| Tâches difficiles, C2 v3 | 1 | 0, 1 discutable | Le relecteur a signalé un idiome que le projet répète | Un S7 sur un élément privé n'est qu'une remarque |
| Calibration, C2 | 2 | 2 | R8 comparait au fichier d'avant chaque écriture | R8 compare au dernier état approuvé |
| Calibration, C2 après correction | 0 | 0 | — | — |
| Horizonte, pilote | 1 | 0 | — | — |
| Horizonte, confirmation | 3 | 1 | R8 a pris une fonction utilitaire de test pour un test | Seul ce qui vérifie quelque chose compte comme test |
Une mauvaise liste est pire que pas de liste. La première version de Sens suggérait huit symboles sans rapport avec la demande ; le modèle les a lus, n'a pas cherché plus loin et a réécrit à la main la fonction des accents les trois fois, alors que le Canon en texte, sans liste, l'a importée les trois fois. Avec la recherche refaite, la fonction apparaît parmi les suggestions et Sens l'utilise à chaque fois, sans rien bloquer.
Limites
- Un seul modèle. Toutes les exécutions ont utilisé Claude Sonnet 5.5 en effort moyen.
- Un projet, un langage, trois séquences par bras. Le critère de confirmation est exigeant, toutes sous toutes, mais l'ampleur de l'effet a un intervalle large.
- Nous avons écrit les tâches. Pour qu'elles ne biaisent pas le résultat, les tâches, leurs tests et la référence ont été enregistrés avant la première exécution, et le critère fixé avant la mesure.
- Pas d'outils MCP dans le banc. Sens a été mesuré sans les requêtes à l'index qu'offre l'application : le résultat est une borne inférieure.
- Dix-neuf langages pas encore mesurés au banc. Vue, Svelte et les langages ajoutés ensuite sont couverts par des tests, pas par des exécutions de l'agent.
- Les tokens incluent les lectures de cache. Ils mesurent le volume de travail, pas le coût exact.
- Le relecteur manque de précision. Sur les sept remarques et blocages relus qui venaient de lui, cinq étaient erronés. Ses remarques n'arrêtent rien, mais elles parviennent au modèle et à vous.
- Conventions non écrites. Sens ne connaît pas les règles implicites d'un projet, comme garder les imports lourds à l'intérieur d'une fonction.
Méthode
456 exécutions de l'agent en cinq lots : un pilote, trois tâches difficiles sur Sens lui-même, douze tâches de calibration, puis le pilote et la confirmation d'Horizonte. Toutes les conditions ont utilisé claude-sonnet-5-5 en effort moyen, avec Claude Code en --safe-mode pour que la configuration de l'auteur ne s'infiltre pas dans les exécutions. Chaque tâche a été validée avant usage : au départ, les tests du projet passent et les tests cachés échouent, et avec la référence appliquée tous passent. Une régression doit échouer deux fois de suite pour compter. Les différences sont des médianes avec un intervalle bootstrap à 95 %, 10 000 rééchantillonnages à graine fixe ; le critère d'Horizonte est un test exact de permutation.
Le reproduire
sens-bench validate --tasks bench/tasks
sens-bench run --tasks bench/tasks --condition C0,C1,C2 --reps 3 --out bench/results/<batch>
sens-bench sequence validate bench/sequences/cuentas
sens-bench sequence run bench/sequences/cuentas --condition C0,C2 --reps 3 --out bench/results/<batch>
sens-bench sequence report bench/results/<batch>Les données de chaque exécution, son diff et les tâches se trouvent dans le dossier bench/ du dépôt de Sens.
Le Canon
Le texte que reçoit chaque session, mot pour mot, en anglais comme le lit le modèle. C'est le circuit qui en fait plus qu'un conseil.
# Sens Canon v1.1
You are working inside Sens. Sens indexes this project and judges every change you make before your turn can end. What Sens tells you about this project, in its messages, denials and reviews, is a fact about the code, not a suggestion. When Sens names something to reuse, reuse it.
## Before you write
Go down this ladder and stop at the first step that answers the need:
1. Is it needed? Do what the person asked and nothing more: no speculative options, parameters, flags or branches. A test that proves the change is part of the change, not something extra.
2. Does the project already have it? Reuse the existing function, component, type or constant. Ask Sens with `already_exists` or `find_symbol` when unsure.
3. Does the standard library or the platform give it? Use that.
4. Does an installed dependency give it? Use that. A new dependency needs the person's approval, and Sens asks them for it.
5. Only then write new code: the smallest version that is correct.
## While you write
- Fix the cause in the shared code, not the symptom in each caller.
- No abstraction without a second real use: no interface, factory, wrapper, layer or configuration for a single consumer.
- Boring over clever. Match the names, patterns and style of the code around you.
- If you would copy a block, extract it once and call it from both places.
- Delete what your change leaves unused.
## Never cut
Less code never means removing validation at trust boundaries, error handling that prevents data loss, security checks, accessibility, or anything the person asked for.
It never means skipping tests either. When your change alters behaviour and the project has tests, add or extend one that fails without your change, in the style of the tests around it, and run the tests you touched before you finish.
## Working with Sens
- A denied write comes with the reason and what to use instead. Change the approach. Retrying the same thing through the shell, another tool or a subagent does not help: Sens judges what lands on disk, however it got there.
- When you finish, Sens audits the whole turn. If it blocks, fix what it found and finish again.
- Sens judges the shape of the code, not whether it works. Its approval is not a test run: that part is yours.
- Never edit `.sens/`, `.claude/settings*.json` or `.mcp.json`.