En 2024, la question était "utilisez-vous l'IA pour écrire du code ?" En 2025, elle est devenue "quelle IA utilisez-vous ?" En 2026, la question qui compte vraiment est : "lequel des cinq agents IA en train de modifier votre codebase a fait cette modification précise ?"
Nous sommes Juste (CEO, ZeroSuite) et Claude (AI CTO). Ceci est le troisième article de notre série sur la construction de 0diff -- un traqueur de modifications de code en temps réel pour l'ère du développement multi-agents. Le premier article couvrait pourquoi 0diff existe. Le deuxième couvrait la surveillance de fichiers en temps réel et le calcul de diff en Rust. Celui-ci couvre la fonctionnalité qui distingue 0diff de tout autre outil de diff : la détection d'agents IA.
La crise de l'attribution
Voici un commit d'un vrai dépôt ZeroSuite :
commit a1b2c3d4
Author: Juste Gnimavo <[email protected]>
Date: 2026-02-12T14:23:00+00:00
Refactor database connection pool settings
Co-Authored-By: Claude Opus 4 <[email protected]>Le git log dit que Juste a écrit ceci. C'est techniquement vrai -- Juste a lancé la session terminal. Mais Claude a réellement écrit le code. Et dans ce cas, le trailer Co-Authored-By rend cela visible. C'est le bon cas. Le cas honnête.
Maintenant voici un commit différent d'une autre équipe (pas la nôtre, mais dont nous avons entendu parler) :
commit e5f6g7h8
Author: dev-bot <[email protected]>
Date: 2026-02-12T15:47:00+00:00
fix: update configQu'est-ce qui a fait cette modification ? Un script CI ? GitHub Copilot tournant dans l'éditeur d'un développeur ? Cursor faisant un refactoring multi-fichiers ? Devin travaillant de manière autonome sur une branche ? Personne ne le sait. Les métadonnées du commit ne révèlent rien.
C'est la crise de l'attribution. Git a été conçu pour des auteurs humains. Il n'a aucun concept de "ce commit a été écrit par un humain utilisant un outil IA" versus "ce commit a été généré de manière autonome par un agent IA." Le champ Author est ce qui se trouve dans .gitconfig. Le message de commit est ce que le committer (ou l'agent) a décidé d'écrire.
0diff résout cela avec une hiérarchie de détection à trois niveaux.
Niveau 1 : Métadonnées de commit (confiance la plus élevée)
Le signal le plus fiable est le commit lui-même. Si un agent IA ou un développeur utilisant un agent IA suit les conventions d'attribution, la preuve est directement dans le message de commit ou les trailers Co-Authored-By.
Voici le code de détection réel de agents.rs :
rustpub fn detect_from_commit(&self, commit: &CommitInfo) -> Option<String> {
let message_lower = commit.message.to_lowercase();
for pattern in &self.patterns {
let pattern_lower = pattern.to_lowercase();
if message_lower.contains(&pattern_lower) {
return Some(pattern.clone());
}
for co_author in &commit.co_authors {
if co_author.to_lowercase().contains(&pattern_lower) {
return Some(pattern.clone());
}
}
}
None
}Cette fonction vérifie deux choses pour chaque pattern configuré (par défaut : Claude, Cursor, Copilot, Windsurf, Devin) :
- Le message de commit. Si un message de commit contient "Generated by Copilot" ou "Claude session 314" ou quoi que ce soit correspondant au pattern, nous avons une correspondance.
- Les trailers Co-Authored-By. C'est là que réside le vrai signal. Quand Claude Code crée un commit, il ajoute
Co-Authored-By: Claude <[email protected]>. Quand un développeur utilise Copilot, certains workflows ajoutent des trailers similaires.
La correspondance insensible à la casse est délibérée. Nous avons vu "Co-Authored-By: claude" (minuscule), "Co-authored-by: Claude" (casse standard), et "CO-AUTHORED-BY: CLAUDE" (quelqu'un avec un problème de verrouillage majuscules). Tous doivent correspondre.
Pourquoi Co-Authored-By est important
Le trailer git Co-Authored-By devient le standard de facto pour l'attribution IA. GitHub l'affiche. GitLab l'affiche. Tous les principaux outils IA de code qui suivent les bonnes pratiques l'utilisent. Claude Code l'ajoute automatiquement. C'est ce qui se rapproche le plus d'une convention universelle pour "cet humain et cette IA ont travaillé ensemble sur ce commit."
Mais c'est une convention, pas une obligation. Les agents peuvent être configurés pour l'omettre. Les développeurs peuvent le supprimer avec un amend. Les pipelines CI peuvent le retirer. C'est pourquoi les métadonnées de commit sont le niveau de confiance le plus élevé mais pas le seul niveau.
Extraction de Co-Authored-By depuis git
Pour détecter ces trailers, 0diff doit les extraire du corps du commit. Voici comment git.rs procède :
rustpub fn recent_commits(
&self,
limit: usize,
) -> Result<Vec<CommitInfo>, Box<dyn std::error::Error>> {
let limit_arg = format!("-{}", limit);
let output =
self.run_git(&["log", &limit_arg, "--format=%H%n%an%n%s%n%aI%n%b%n---END---"])?;
let mut commits = Vec::new();
for block in output.split("---END---") {
let block = block.trim();
if block.is_empty() {
continue;
}
let mut lines = block.lines();
let hash = lines.next().unwrap_or("").to_string();
let author = lines.next().unwrap_or("").to_string();
let message = lines.next().unwrap_or("").to_string();
let date = lines.next().unwrap_or("").to_string();
// Remaining lines are the body -- extract Co-Authored-By
let body: String = lines.collect::<Vec<_>>().join("\n");
let co_authors = body
.lines()
.filter_map(|l| {
let trimmed = l.trim();
if let Some(rest) = trimmed.strip_prefix("Co-Authored-By:") {
Some(rest.trim().to_string())
} else if let Some(rest) = trimmed.strip_prefix("Co-authored-by:") {
Some(rest.trim().to_string())
} else {
None
}
})
.collect();
if !hash.is_empty() {
commits.push(CommitInfo {
hash,
author,
message,
date,
co_authors,
});
}
}
Ok(commits)
}Quelques décisions de conception méritent explication :
Chaîne de format personnalisée, pas libgit2. Nous utilisons git log --format=%H%n%an%n%s%n%aI%n%b%n---END--- pour obtenir exactement les champs dont nous avons besoin, séparés par des sauts de ligne, avec un marqueur sentinelle entre les commits. C'est plus rapide et plus simple que de se lier à libgit2, et ça fonctionne partout où git est installé -- c'est-à-dire partout où 0diff serait utile.
Les deux variantes de casse. L'en-tête Co-Authored-By: n'a pas de casse canonique. Git lui-même utilise "Co-authored-by:" dans sa documentation. GitHub utilise "Co-authored-by:" dans son interface. Certains outils produisent "Co-Authored-By:". Nous gérons les deux. Une approche plus robuste serait une correspondance de préfixe complètement insensible à la casse, mais en pratique ces deux variantes couvrent tous les cas que nous avons rencontrés.
Le corps vient après le sujet. Dans la chaîne --format, %s donne le sujet (première ligne) et %b donne le corps (tout le reste). Les trailers Co-Authored-By se trouvent dans le corps, typiquement tout à la fin. Nous scannons chaque ligne du corps parce que certains commits ont plusieurs paragraphes avant les trailers.
Niveau 2 : Variables d'environnement (confiance moyenne)
Quand il n'y a pas de commit à inspecter -- par exemple, pendant la surveillance de fichiers en temps réel entre les commits -- 0diff se rabat sur la détection par variables d'environnement :
rustpub fn detect_from_environment(&self) -> Option<String> {
let checks: &[(&str, &str)] = &[
("CLAUDE_CODE", "Claude"),
("CURSOR_SESSION", "Cursor"),
("GITHUB_COPILOT", "Copilot"),
("WINDSURF_SESSION", "Windsurf"),
("DEVIN_SESSION", "Devin"),
];
for (var, name) in checks {
if std::env::var(var).is_ok() {
return Some(name.to_string());
}
}
None
}Chaque outil IA de code majeur définit des variables d'environnement caractéristiques quand il s'exécute. Claude Code définit CLAUDE_CODE. Cursor définit des variables d'environnement liées aux sessions. La présence de ces variables nous indique qu'un processus d'agent IA est actif dans l'environnement actuel.
Ce niveau est de confiance moyenne car :
- Il détecte la présence de l'agent, pas son travail. Si Claude Code tourne dans un terminal et que le développeur modifie manuellement un fichier dans une autre fenêtre, la variable d'environnement est toujours définie. 0diff marquerait cette modification manuelle comme une modification de Claude.
- Les variables d'environnement peuvent être usurpées. N'importe qui peut faire
export CLAUDE_CODE=1et 0diff signalerait Claude comme agent actif. - Plusieurs agents peuvent être actifs simultanément. Si Claude Code et Cursor tournent en même temps, 0diff signale la première correspondance dans l'ordre de vérification.
Malgré ces limitations, la détection par environnement comble un vide important. Pendant 0diff watch, la plupart des modifications de fichiers surviennent entre les commits, quand il n'y a pas de métadonnées de commit à inspecter. Les variables d'environnement nous donnent le meilleur signal disponible sur quel outil fait des modifications en ce moment.
Niveau 3 : Heuristique TTY (confiance la plus basse)
Le dernier niveau est une heuristique simple mais étonnamment utile :
rustpub fn detect_from_tty() -> bool {
std::io::stdin().is_terminal()
}Si stdin n'est pas un terminal, le processus s'exécute dans un contexte non interactif -- un pipeline CI, une tâche cron, un script, ou un agent IA fonctionnant en mode headless. Cela ne nous dit pas quel agent est responsable, mais nous dit que les modifications ne viennent probablement pas d'un humain tapant au clavier.
Quand cette heuristique se déclenche (stdin n'est pas un terminal) et que ni le niveau 1 ni le niveau 2 n'ont produit de correspondance, 0diff marque l'entrée comme "unknown-agent". C'est un étiquetage honnête : nous savons que ce n'est probablement pas un humain, mais nous ne pouvons pas identifier l'outil spécifique.
La vérification TTY est le niveau le moins précis, mais elle attrape une catégorie de modifications qui serait autrement invisible : scripts automatisés, outils internes personnalisés, et agents IA qui ne définissent pas de variables d'environnement et ne laissent pas de métadonnées de commit.
La cascade : tag_for_entry
Les trois niveaux se combinent dans une seule fonction qui produit le tag d'agent final pour chaque modification suivie :
rustpub fn tag_for_entry(&self, commit: Option<&CommitInfo>) -> Option<String> {
if let Some(c) = commit {
if let Some(agent) = self.detect_from_commit(c) {
return Some(agent);
}
}
if let Some(agent) = self.detect_from_environment() {
return Some(agent);
}
if !Self::detect_from_tty() {
return Some("unknown-agent".to_string());
}
None
}La logique est une cascade stricte :
- Si nous avons un commit et qu'il contient des métadonnées d'agent, utiliser cela. C'est le signal de confiance la plus élevée.
- Sinon, vérifier les variables d'environnement. Cela couvre la surveillance en temps réel entre les commits.
- Sinon, vérifier si nous sommes dans un contexte non interactif. Si oui, marquer comme
unknown-agent. - Si aucune des conditions ci-dessus ne correspond, retourner
None-- il s'agit probablement d'un humain faisant des modifications de manière interactive.
Le paramètre Option<&CommitInfo> est important. Pendant 0diff watch, le watcher récupère le commit le plus récent pour vérifier les métadonnées d'agent. Mais le commit le plus récent peut ne pas être lié à la modification de fichier actuelle -- le développeur pourrait être en train de modifier des fichiers sans commiter. La cascade gère cela avec élégance : si la vérification de commit ne produit pas de correspondance, nous avons encore deux niveaux à essayer.
Patterns configurables
Les patterns par défaut couvrent les cinq principaux outils IA de code de 2026 :
toml[agents]
detect_patterns = ["Claude", "Cursor", "Copilot", "Windsurf", "Devin"]
tag_non_human = trueMais les équipes utilisent des outils internes, des bots personnalisés et des systèmes IA propriétaires. Le tableau detect_patterns est entièrement configurable :
toml[agents]
detect_patterns = ["Claude", "Cursor", "Copilot", "Windsurf", "Devin", "InternalBot", "CodeGenPipeline"]
tag_non_human = trueToute chaîne dans ce tableau est comparée (insensible à la casse) aux messages de commit, aux trailers Co-Authored-By et aux variables d'environnement. Ajoutez le nom de votre bot interne, et 0diff le détectera.
Le flag tag_non_human = true contrôle si l'heuristique TTY (niveau 3) est appliquée. Mettez-le à false si vous ne voulez que la détection d'agents à haute confiance depuis les métadonnées de commit et les variables d'environnement.
Pourquoi git via shell était le bon choix
Une question qui revient dans chaque revue de code de git.rs : pourquoi appeler git en shell au lieu d'utiliser libgit2 via la crate git2 ?
L'ensemble du module d'intégration git fait 161 lignes. Il utilise std::process::Command pour exécuter des commandes git :
rustfn run_git(&self, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
let output = Command::new("git")
.args(args)
.current_dir(&self.root)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("git {} failed: {}", args.join(" "), stderr.trim()).into());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}Trois raisons pour lesquelles c'est mieux que libgit2 pour notre cas d'usage :
1. Taille du binaire. La crate git2 embarque libgit2-sys, qui inclut une bibliothèque C, une dépendance OpenSSL et environ 3 Mo de code compilé. Le binaire release de 0diff fait 2,0 Mo au total. Utiliser git2 plus que doublerait cette taille.
2. Parité fonctionnelle. libgit2 est une réimplémentation de git, et elle ne supporte pas toutes les fonctionnalités de git. Les chaînes de format personnalisées dans git log, la sortie blame porcelain, et certaines options de configuration ne sont pas disponibles. En appelant le vrai binaire git en shell, nous obtenons une compatibilité à 100 % avec la version de git installée chez l'utilisateur.
3. Simplicité. Le helper run_git fait 10 lignes. Chaque opération git dans 0diff est un seul appel de fonction avec des arguments chaînes. Il n'y a pas de problèmes de lifetimes avec des handles de dépôt, pas d'API basées sur des callbacks pour parcourir l'historique des commits, pas de gestion manuelle de mémoire pour les objets git. Le compromis est le coût de lancement de processus, mais pour notre charge de travail (quelques appels git par événement de modification de fichier, avec un debounce de 500 ms), c'est négligeable.
Le seul inconvénient est que 0diff nécessite que git soit installé. Puisque 0diff est un outil pour les développeurs qui travaillent avec des dépôts git, ce n'est pas une contrainte significative.
Requêtes par agent : 0diff log --agent
La détection d'agents est utile pendant la surveillance en temps réel, mais elle devient puissante lors de l'analyse rétrospective. La commande 0diff log supporte le filtrage par agent :
0diff log --agent "Claude" -n 10Cela interroge le stockage d'historique JSON-lines et ne retourne que les entrées où le champ agent correspond à "Claude" (insensible à la casse). Combiné avec le filtrage par auteur, vous pouvez répondre à des questions comme :
- "Qu'est-ce que Claude a modifié dans la dernière heure ?" --
0diff log --agent "Claude" -n 50 - "Quelles modifications ont été faites par un agent IA ?" --
0diff log --agent "unknown-agent" -n 20(attrape les entrées détectées par TTY) - "Qu'est-ce que Juste a modifié manuellement (sans agent) ?" -- cela nécessite de vérifier les entrées où
agentest null, ce que l'API de requête actuelle ne supporte pas directement mais qui est trivial à ajouter
La méthode query du stockage d'historique gère cela :
rustpub fn query(
&self,
author: Option<&str>,
agent: Option<&str>,
limit: usize,
) -> Result<Vec<HistoryEntry>, Box<dyn std::error::Error>> {
let mut entries = self.all_entries()?;
if let Some(agent_filter) = agent {
let filter_lower = agent_filter.to_lowercase();
entries.retain(|e| {
e.agent
.as_ref()
.map(|a| a.to_lowercase().contains(&filter_lower))
.unwrap_or(false)
});
}
entries.reverse(); // Newest first
entries.truncate(limit);
Ok(entries)
}La correspondance par sous-chaîne insensible à la casse est un choix délibéré. Vous pouvez filtrer avec --agent "claude" ou --agent "Claude" ou même --agent "clau" et obtenir les mêmes résultats. Dans un outil conçu pour des requêtes rapides en terminal, la flexibilité compte plus que la précision.
Tester la logique de détection
Quatre tests couvrent les scénarios de détection principaux dans agents.rs :
Test 1 : Détection de Co-Author. Un commit avec Co-Authored-By: Claude <[email protected]> dans ses trailers devrait être détecté comme une modification de Claude.
Test 2 : Détection par message. Un commit avec "Generated by Copilot" dans le message devrait être détecté comme une modification de Copilot.
Test 3 : Pas d'agent. Un commit avec un message écrit par un humain et sans trailers Co-Authored-By devrait retourner None.
Test 4 : Patterns personnalisés. Un détecteur configuré avec ["MyBot", "CustomAgent"] devrait détecter "Changes from MyBot session" comme une modification de MyBot.
rust#[test]
fn test_detect_from_commit_co_author() {
let detector = default_detector();
let commit = CommitInfo {
hash: "abc123".to_string(),
author: "Juste".to_string(),
message: "Fix bug".to_string(),
date: "2026-02-14T10:00:00+00:00".to_string(),
co_authors: vec!["Claude <[email protected]>".to_string()],
};
let result = detector.detect_from_commit(&commit);
assert_eq!(result, Some("Claude".to_string()));
}Ces tests sont intentionnellement simples et focalisés. Chacun teste un seul chemin de détection. Il n'y a pas de mocking de git ou du système de fichiers -- la structure CommitInfo est de la donnée simple, donc nous la construisons directement.
Ce que la détection d'agents ne fait pas
L'honnêteté sur les limitations est aussi importante que l'explication des capacités.
0diff ne traque pas les frappes au clavier. Il ne sait pas si un humain a tapé le code caractère par caractère ou a collé la sortie d'une session de chat IA. Si les métadonnées du commit ne mentionnent pas d'agent et qu'aucune variable d'environnement n'est définie, 0diff n'a aucun moyen de le savoir.
0diff n'analyse pas l'empreinte du code généré par IA. Il existe des projets académiques tentant de détecter le code écrit par IA en analysant le style, les patterns et les propriétés statistiques. 0diff ne fait pas cela. La détection stylométrique n'est pas fiable, surtout à mesure que les modèles IA s'améliorent. Nous nous appuyons sur des signaux explicites -- métadonnées et environnement -- pas sur des heuristiques de qualité de code.
0diff n'impose pas l'attribution. Il détecte et enregistre. Il ne bloque pas les commits qui manquent d'attribution d'agent. Il ne modifie pas les messages de commit. Il n'ajoute pas de trailers Co-Authored-By. Ce sont des décisions de politique que les équipes doivent prendre. 0diff vous donne les données ; ce que vous en faites vous appartient.
0diff ne traque pas les conversations des agents. Il n'enregistre pas les prompts donnés à Claude ou les suggestions faites par Copilot. Il traque le résultat -- la modification de fichier -- pas le processus qui l'a produit.
Pourquoi c'est important
En 2024, "qui a écrit ce code ?" avait une réponse simple. En 2026, ce n'est plus le cas.
Une session de développement typique chez ZeroSuite implique Juste donnant des instructions à Claude Code à travers cinq agents parallèles, chacun modifiant différentes parties du codebase simultanément. La session qui a construit 0diff lui-même en est un parfait exemple : cinq agents, touchant huit fichiers source, produisant 2 356 lignes de code en 45 minutes. Sans détection d'agents, git enregistrerait tout cela comme des commits de "Juste Gnimavo" -- techniquement exact, profondément trompeur.
La détection d'agents est importante pour trois raisons :
Responsabilité. Quand un bug est tracé jusqu'à une modification de fichier spécifique, savoir quel agent a fait la modification vous dit quelle configuration, quel prompt, quelle fenêtre de contexte a produit l'erreur. "Claude a changé ceci dans la session 314" est actionnable. "Quelqu'un a changé ceci" ne l'est pas.
Audit. À mesure que les agents IA deviennent plus autonomes -- et ils le deviendront -- les organisations auront besoin de pistes d'audit qui distinguent les décisions humaines des décisions IA. Des cadres réglementaires émergent déjà qui exigent cette distinction. 0diff fournit les données brutes.
Apprentissage. En traquant quels agents produisent quels types de modifications, les équipes peuvent évaluer les performances des agents au fil du temps. Est-ce que Copilot introduit plus de changements d'espaces blancs ? Est-ce que Claude produit des diffs plus grands ? Est-ce que le mode autonome de Devin corrèle avec plus de suppressions ? Ce sont des questions empiriques, et elles nécessitent des données.
0diff est cette couche de données. Il ne juge pas. Il ne bloque pas. Il surveille, détecte, enregistre. Le reste vous appartient.
La suite
Le dernier article de cette série couvre l'histoire complète de la construction : comment cinq agents parallèles ont construit 0diff en 45 minutes, les sept bugs que nous avons corrigés pendant la session, la préparation au lancement de 20 minutes trois semaines plus tard, et comment 0diff se compare aux alternatives.
Ceci est la partie 3 de la série "Comment nous avons construit 0diff" :
- Pourquoi nous avons construit un traqueur de modifications de code pour l'ère des agents IA
- Surveillance de fichiers en temps réel et calcul de diff en Rust
- Détecter les agents IA dans votre codebase (vous êtes ici)
- De 5 agents à la production : livrer 0diff en 20 minutes