L’utilisateur le plus fréquent de mon CLI, ce n’est pas moi

lql est un CLI en Rust pour gérer les issues Linear. Je l’ai écrit parce qu’aucune des alternatives existantes ne fonctionnait pour mon cas d’usage réel : un agent IA qui gère les issues de façon autonome.

Pourquoi j’ai dû écrire mon propre CLI

Le MCP de Linear fut la première tentative. L’idée est élégante : un serveur MCP qui expose l’API Linear directement à l’agent. En pratique, c’était lent, instable, et l’agent devait construire les requêtes GraphQL depuis zéro à chaque appel. Chaque appel était une opportunité d’inventer un champ qui n’existe pas. Je l’ai désinstallé au bout de deux semaines.

Le CLI de la communauté (linear, de schpet) fut la seconde tentative. Conçu pour les humains : menus interactifs, sélection avec les flèches, invites de confirmation. Un agent ne peut pas naviguer dans des menus interactifs. Suivant.

L’« agent » de Linear. En mars 2026, Linear a annoncé son propre agent avec IA. Ça sonne parfait jusqu’à ce qu’on lise les petites lignes : ne fonctionne que dans l’interface web de Linear. Impossible d’appeler depuis le terminal, pas d’API, ne s’intègre à rien d’externe. C’est un chatbot collé à leur propre UI. Si votre flux de travail est « l’agent qui programme gère aussi les issues », l’agent de Linear ne résout rien.

Conception basée sur les données : la première analyse

Avant d’écrire lql, j’ai parsé 165 sessions de Claude Code en cherchant chaque erreur lors d’interactions avec Linear. Le résultat : plus de 500 erreurs, 370 nouvelles tentatives, et une estimation conservatrice de 700'000 tokens gaspillés par mois. --sort oublié 40 fois. --state "Todo" au lieu de --state unstarted, 12 fois. --no-interactive absent et le CLI suspendu en attente d’input clavier, 64 fois. (Les détails complets sont dans le post original.)

Avec ces données, j’ai conçu l’interface de lql. Je n’ai pas deviné ce dont un agent avait besoin — je l’ai mesuré. De là sont sorties les décisions fondamentales : output compact en TOON (~25 tokens par issue au lieu de JSON verbeux), alias pour les flags que les LLMs confondent (--status--state), normalisation des valeurs (Todounstarted, urgent1), et messages d’erreur qui suggèrent la commande correcte au lieu de se contenter de dire « flag inconnu ».

Sans le savoir, j’appliquais la loi de Postel. Mais ça, je l’ai découvert après.

La seconde analyse : un mois avec lql en production

lql tourne en production depuis un mois. Claude Code l’appelle entre 30 et 50 fois par jour — créant des issues, mettant à jour les états, liant des dépendances, consultant des détails. Moi, je ne l’appelle jamais. Ça ne m’intéresse pas. Je ne veux pas gérer les issues à la main ; j’ai un agent pour ça. Si un jour je dois exécuter lql moi-même, c’est que quelque chose a mal tourné.

Le seul utilisateur de lql est un LLM. Ça change toute la conception.

J’ai donc répété l’exercice : j’ai parsé les logs de session de Claude Code en cherchant les erreurs lors d’appels à lql.

MétriqueValeur
Sessions analysées200
Appels de lql1'324
Erreurs (is_error: true)210
Taux d’erreur15,9%

Les 15,9% n’incluent pas les appels annulés par parallélisme (quand un tool call échoue et Claude annule les autres de ce batch). Seulement les vraies erreurs du CLI.

Classification des erreurs

Toutes les erreurs ne se valent pas. Certaines révèlent des conventions manquantes ; d’autres révèlent des opérations qui devraient exister.

ErreurFréquenceExemple réel
Label non trouvé20--label tokamak (n’existe pas dans ce team)
--title comme flag dans create8lql create --title "Epic: ..." --team PROD
show/get au lieu de view6lql show PROD-911
Args de relate dans mauvais ordre12lql relate PROD-834 PROD-833 blocked-by
update --team (déplacer issue)15+ tentativeslql update PRIV-32 --team PROD
relates au lieu de related2lql relate PROD-912 relates PROD-910
--body dans comment1lql comment PROD-926 --body "texte"
--comments dans view1lql view PROD-824 --comments

Le reste, c’étaient des erreurs de l’API Linear, des problèmes d’authentification avec 1Password, ou des erreurs de shell (quoting cassé dans de longs heredocs).

Ce que révèlent les erreurs

Les LLMs ne lisent pas --help. Ils devinent par intuition sémantique.

Quand un développeur ne sait pas comment fonctionne une commande, il exécute lql view --help. Quand Claude ne sait pas, il devine. Et il devine bien 84% du temps — mais les 16% restants révèlent ses biais.

lql show est plus intuitif que lql view. La plupart des outils utilisent show : kubectl get, docker inspect, git show. Claude ne consulte pas la documentation de lql pour choisir le verbe. Il utilise celui qui lui paraît le plus naturel vu les milliers de CLIs qu’il a vus dans ses données d’entraînement.

La solution n’est pas de mieux documenter. C’est d’accepter le synonyme :

#[command(alias = "show", alias = "get")]
View(ViewOpts),

Une ligne. Six erreurs éliminées.

Les agents préfèrent les flags nommés aux arguments positionnels

Dans lql create, le titre est un argument positionnel :

lql create "Mon titre" --team PROD

Claude, à 8 reprises, l’a écrit ainsi :

lql create --title "Mon titre" --team PROD

--title n’existait pas comme flag. Pour un humain, c’est évident — on lit le --help et on voit que <TITLE> est positionnel. Pour un LLM, les flags nommés sont plus sûrs car ils ne dépendent pas de la position.

La correction : accepter les deux.

pub struct CreateOpts {
    pub title: Option<String>,

    #[arg(long = "title", hide = true)]
    pub title_flag: Option<String>,
    // ...
}

Le flag --title est caché dans --help (les humains n’en ont pas besoin), mais il fonctionne.

Si vous pouvez détecter l’erreur, corrigez-la au lieu de la rejeter

Le cas le plus révélateur. lql relate attend trois arguments positionnels dans un ordre strict :

lql relate <FROM> <RELATION_TYPE> <TO>

Claude a écrit ceci 12 fois :

lql relate PROD-834 PROD-833 blocked-by

L’ordre naturel pour un LLM est FROM TO TYPE — « relie ceci à cela, de cette façon ». L’ordre du CLI est FROM TYPE TO — « depuis ici, type de relation, vers là ».

La philosophie POSIX dit : rejette l’entrée incorrecte avec une erreur descriptive. La philosophie AX dit : si vous pouvez détecter que le second argument est un ID d’issue et le troisième est un type de relation, réordonnez-les automatiquement.

pub fn normalize_args(args: &[String]) -> Option<Vec<String>> {
    if args.len() < 5 { return None; }
    if args[1] == "relate"
        && looks_like_issue_id(&args[2])
        && looks_like_issue_id(&args[3])
        && !looks_like_issue_id(&args[4])
    {
        let mut fixed = args.to_vec();
        fixed.swap(3, 4);
        eprintln!(
            "ℹ Réordonné : relate {} {} {} → relate {} {} {}",
            args[2], args[3], args[4], fixed[2], fixed[3], fixed[4]
        );
        return Some(fixed);
    }
    None
}

La détection est déterministe : un ID d’issue a le format TEAM-123 (majuscules, tiret, nombre). Un type de relation, non. Aucune ambiguïté possible.

Une note est émise sur stderr (ℹ Réordonné : ...) pour qu’il y ait trace de la correction. Si jamais l’heuristique échoue, l’utilisateur peut tracer ce qui s’est passé.

Si une opération est tentée de façon répétée, elle devrait exister

L’agent a tenté lql update PRIV-32 --team PROD plus de 15 fois au cours de multiples sessions. Déplacer une issue d’une équipe à l’autre est une opération légitime de Linear que lql n’implémentait tout simplement pas.

Ce n’était pas une erreur d’interface. C’était une fonctionnalité qui manquait. Les données l’ont rendu visible.

Ajouter --team à update a requis 3 lignes dans le parser clap et un appel supplémentaire à meta.find_team() dans la logique de mise à jour. L’API Linear supportait déjà teamId dans la mutation issueUpdate.

Ce qui NE se corrige PAS avec la tolérance

Il faut être honnête sur les limites. Sur les 210 erreurs :

  • 20 étaient des labels qui n’existent pas. Claude inventait des labels comme tokamak ou improvement qui ne sont pas dans cette équipe Linear. Ça ne se corrige pas avec des alias — il faut que l’agent consulte les labels disponibles avant de créer. lql retourne déjà des suggestions fuzzy (« Closest : … »), mais Claude ne retente pas toujours.

  • 18 étaient des erreurs de l’API (labels d’équipe incorrects, requêtes GraphQL invalides dans raw). Ce sont des erreurs de l’agent, pas du CLI.

  • 7 étaient d’authentification (1Password en panne ou session expirée). Infrastructure, pas interface.

La tolérance d’interface couvre peut-être 60% des erreurs. Le reste nécessite que l’agent soit plus discipliné ou que l’outil valide plus de choses avant d’envoyer à l’API.

La loi de Postel appliquée aux CLIs

Jon Postel a écrit en 1980 : “Be conservative in what you send, be liberal in what you accept” (RFC 761). C’est le principe de robustesse de TCP. Tous les protocoles internet qui fonctionnent l’appliquent.

Personne ne l’applique aux CLIs. L’orthodoxie POSIX est l’inverse : rejeter tout input qui ne correspond pas exactement à la spécification, retourner une erreur descriptive, et laisser l’utilisateur corriger. Quand votre utilisateur est un humain qui lit l’erreur et ajuste, ça fonctionne. Quand votre utilisateur est un LLM qui retente avec une variation aléatoire, c’est une perte de temps et de tokens.

L’expérience agentique, c’est la loi de Postel appliquée aux arguments de CLI. Ce n’est pas une idée nouvelle. C’est un principe de 1980 qui n’a jamais été appliqué à ce contexte.

Des données émergent cinq règles concrètes :

  1. Acceptez les synonymes naturels. Si le verbe existe dans des CLIs populaires (show, get, display), acceptez-le comme alias. Ça ne coûte rien et élimine les erreurs de vocabulaire.

  2. Acceptez les flags nommés en plus des positionnels. Les LLMs préfèrent --title "X" à mettre "X" dans la position correcte. Cachez-les dans --help si vous ne voulez pas embrouiller les humains.

  3. Réordonnez avant de rejeter. Si les arguments sont de types distinguables (ID d’issue vs. string enum), l’ordre incorrect peut être détecté et corrigé automatiquement.

  4. Normalisez les variantes proches. relatesrelated, blockedbyblocked-by. La distance d’édition est de 1. Le coût de l’accepter est zéro. Le coût de le rejeter est une erreur et une nouvelle tentative.

  5. Si c’est tenté >3 fois, ça devrait probablement exister. Les logs de session sont une mine de données sur les fonctionnalités manquantes. Un agent n’insiste pas sur une opération absurde 15 fois. S’il insiste, l’opération a du sens et l’outil ne la supporte pas.

Le méta-angle

J’ai utilisé Claude pour parser les logs de Claude, classifier les erreurs de Claude en utilisant mon outil, puis implémenter les corrections. L’outil s’adapte à son utilisateur le plus fréquent avec les données de ce même utilisateur.

Tout le code est public. Le commit avec les corrections est 34f1f08. Les données peuvent être reproduites en parsant les JSONL dans ~/.claude/projects/.

Comment le faire vous-même

  1. Parsez les logs. Les fichiers JSONL de Claude Code sont dans ~/.claude/projects/<project>/. Chaque tool_result avec is_error: true est une donnée. Le format est le même pour n’importe quel outil, pas seulement pour les CLIs.

  2. Classifiez avant de corriger. Toutes les erreurs ne se valent pas. Séparez les erreurs d’interface (le CLI rejette un input valide) des erreurs de logique (l’agent demande quelque chose d’absurde). Seules les premières se corrigent avec la tolérance.

  3. Mesurez après. Le taux d’erreur avant les changements était de 15,9%. La prochaine fois que j’analyserai, je saurai s’il a baissé. Sans la première mesure, pas de baseline.

lql s’installe avec brew install frr149/tools/lql. Le repo est sur github.com/frr149/lql.

Cet article a été rédigé en espagnol et traduit avec l’aide de l’IA.