Wu Wei

ponytail : moins de code, du meilleur code ?

· #agents #outils

Agent Report

Outil
ponytail (76,5k ★)
Type
Critique
Contexte
full-stack-fastapi-template (moyen) + Django (grand, 522k LOC) — 4 tâches A/B, modèle fort
Coût
8 sessions d'agent (baseline vs. ruleset) ; LOC mesuré avec git diff

ponytail est en tendance sur GitHub depuis des jours : 76,5k étoiles, un slogan parfait (« ne dit rien, écrit une ligne, ça marche ») et une promesse concrète — que ton agent IA écrive ~54% de code en moins (jusqu'à 94%), ~20% moins cher, 100% sûr. C'est un plugin qui injecte un ruleset —une « échelle de paresse »— dans 16+ agents (Claude Code, Cursor, Codex…) : avant d'écrire du code, l'agent s'arrête au premier barreau qui tient — est-ce nécessaire ? existe-t-il déjà dans le dépôt ? la stdlib le fait-elle ? une fonctionnalité native ? une dépendance déjà installée ? ça tient en une ligne ? — et seulement ensuite écrit le minimum.

DietrichGebert/ponytail ★ 76,5k

Le rare : son benchmark est honnête

Presque tout projet viral gonfle ses chiffres. ponytail fait l'inverse. Son benchmark reconnaît que le chiffre original « 80-94% de code en moins » était gonflé par un baseline bavard (signalé par Colin Eberhardt dans #126), a trouvé et publié un bug de contamination dans sa propre mesure (un hook qui activait ponytail sur le baseline aussi), a refait le test pour pouvoir se réfuter, et a corrigé le chiffre à un -54% agentique défendable, en montrant où il ne gagne pas. C'est l'inverse de la norme. Il n'y a pas de « gotcha » à faire ici.

Mais ce benchmark a des limites qu'ils listent eux-mêmes : un seul modèle (Haiku 4.5, petit, qui sur-construit davantage et a besoin de plus de guidage), un dépôt template moyen (full-stack-fastapi-template) et des tickets de features greenfield où le tour de force —« un date picker de 404 lignes → <input type="date"> »— domine la moyenne. Leur propre tableau montre déjà le CRUD backend converger vers ~0%. C'est là qu'intervient cette étude.

Ce que j'ai testé et pas eux

Leurs limites sont mon test : un modèle fort (pas Haiku), un dépôt vraiment grand et mature (Django, 522k lignes de Python, 7 072 fichiers) et des axes plus difficiles qu'ajouter un widget — réutiliser un helper qui existe déjà dans un énorme codebase, écrire un utilitaire neuf, et une tâche « irréductible ». Une trace longue, pas deux exemples.

Méthode (la leur, étendue) : A/B du même agent avec le ruleset de ponytail injecté vs. sans lui (baseline = l'agent faisant le travail correctement, pas un modèle bavard — la correction juste qu'ils ont faite eux-mêmes). Quatre tickets réels sur des dépôts réels, chaque bras dans sa propre copie du dépôt, LOC mesuré comme lignes ajoutées du git diff — leur propre métrique.

Tâche 1 — le color picker (moyen, sur-construction)

Ticket : « ajoute un sélecteur de couleur au formulaire d'item ». C'est leur type de tâche signature. Résultat : baseline 168 lignes, ponytail 41 — une coupe de -76%. Mais le détail compte : aucun n'a installé de bibliothèque. Les deux sont allés droit à l'input natif du navigateur :

ponytail — color picker
<FormField
  control={form.control}
  name="color"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Color</FormLabel>
      <FormControl>
        <Input type="color" className="h-9 w-16 p-1" {...field} />
      </FormControl>
    </FormItem>
  )}
/>

Sur un modèle fort, le piège « installe flatpickr et monte un composant wrapper » —la source du -94% sur Haiku— se déclenche à peine : le baseline a aussi utilisé <input type="color">. Alors d'où vient le -76% ? Du scope. Le baseline a fait passer la couleur de bout en bout : modèle backend, migration Alembic, colonne de table, validation du hex, champ texte à côté du swatch. ponytail n'a fait que le formulaire frontend — et la couleur n'est jamais stockée en base (son propre rapport l'admet : « backend hors du scope du ticket »). Le ticket était ambigu ; ponytail a pris la lecture la plus étroite. Plus propre, oui — mais la coupe a laissé un vrai trou.

Tâche 2 — réutiliser dans 522k lignes (Django, la claim difficile)

Le barreau 2 (« déjà dans le codebase ? réutilise-le ») est facile à écrire dans un ruleset et difficile à honorer : il exige que l'agent trouve le helper dans un énorme dépôt. Ticket : « une fonction title_to_slug qui transforme un titre en slug ». Django a django.utils.text.slugify enterré parmi 522k lignes. Le trouve-t-il ?

Oui — et ça marche. ponytail l'a localisé et a écrit un wrapper d'une ligne :

ponytail — title_to_slug
from django.utils.text import slugify


def title_to_slug(title):
    """
    >>> title_to_slug("Hello, World!")
    'hello-world'
    """
    # ponytail: django.utils.text.slugify already does the work; just wrap it.
    return slugify(title)

baseline 18 lignes, ponytail 10 (-44%). Mais voici la nuance : le baseline a aussi réutilisé slugify. La décision importante —ne pas réinventer le slug avec une regex— a été prise par les deux. La coupe de ponytail était de la cérémonie (docstring de module + bloc __main__ vs. one-liner + doctest), pas une décision plus maligne. Un agent compétent monte déjà au barreau 2 sans qu'on le lui dise.

Tâche 3 — un utilitaire neuf dans le grand dépôt

Ticket : une fonction retry avec backoff exponentiel. Pas de retry public dans Django ni la stdlib, donc les deux l'écrivent de zéro — terrain fertile pour sur-construire (un décorateur configurable ? du jitter ? une classe de politique ?). Résultat : baseline 43, ponytail 32 (-26%). Ils convergent presque. Aucun n'a sur-construit : les deux ont écrit une fonction sobre avec time.sleep de la stdlib et laissé un check exécutable. La vraie différence :

baseline — le guard que ponytail a sauté
def retry(fn, attempts=3, base_delay=0.1):
    if attempts < 1:
        raise ValueError("attempts must be at least 1.")   # baseline l'a mis
    ...

Le baseline a ajouté une validation d'entrée (attempts < 1) que ponytail a sautée. Ironique : le ruleset de ponytail dit lui-même « pas paresseux sur la validation aux frontières de confiance ». Ici, une partie de la coupe de ponytail était précisément ce guard.

Tâche 4 — l'endpoint « trivial »

Ticket : « un endpoint qui renvoie combien d'items possède l'utilisateur courant ». J'attendais une convergence (c'est du CRUD). Ce fut l'inverse : baseline 71 lignes, ponytail 12 — une coupe de -83%. ponytail a écrit un endpoint propre, réutilisé la requête de comptage existante, et même ordonné correctement la route (/count avant /{id}, un vrai détail de routing). Mais :

base
71 lignes

Endpoint + un modèle de réponse typé (OwnedItemsCount) + 3 tests : compte 0→3, ne compte pas les items d'autrui, exige l'auth.

pony
12 lignes

Endpoint renvoyant un dict brut. Pas de modèle typé, aucun test.

ponytail a renvoyé un dict non typé —brisant la convention de réponses typées que tout le reste du dépôt utilise (son propre barreau 2)— et n'a laissé aucun test, alors que sa règle dit « la logique non triviale laisse UN check exécutable ». Il a jugé l'endpoint trivial. Le baseline a écrit trois tests qui vérifient des choses réelles (qu'il ne compte pas les items d'autres utilisateurs, qu'il exige l'authentification). Le baseline a-t-il sur-testé, ou ponytail sous-testé ? Les avis raisonnables divergent — mais la coupe de -83% n'était pas du bloat, c'était de la couverture et des types.

Le motif

Moyenne des quatre coupes : ~57% — presque exactement leur -54%, même sur un modèle fort et un grand dépôt. Le chiffre de LOC se reproduit. Ce qui change, c'est d'où vient la coupe :

Sur Haiku, le -54% vient surtout d'éviter de vraies sur-constructions (le date picker de 404 lignes). Sur un modèle fort, le baseline évite déjà ces pièges — donc la coupe de ponytail se déplace vers moins de scope (T1 : couleur non persistée), moins de cérémonie (T2), presque rien (T3) et moins de tests et de types (T4). Dans 2 des 4 tâches, la coupe a enlevé quelque chose qu'un relecteur voudrait récupérer.

Ce n'est pas un verdict contre lui. ponytail n'a jamais écrit plus (sa claim centrale tient), a bien réutilisé dans tous les cas —y compris trouver slugify dans un demi-million de lignes— et son équipe est l'une des rares à mesurer honnêtement. Mais « moins de code » n'est pas automatiquement « du meilleur code » : sur un modèle capable le gain est modeste et coupe parfois du scope, des tests ou un guard.

Ce que d'autres savaient déjà

Ce n'est pas que ma mesure. Un benchmark indépendant (#236, KuldeepB19 : 480 builds, 24 jobs, Claude Opus 4.8, quatre niveaux) a trouvé ~44% de code en moins sans perte générale de correctness ni de sécurité — mais un coût de robustesse réel sur 5 jobs avec des edge-cases non dits : « un build a planté sur une mauvaise entrée là où la version sans plugin a tenu », et le niveau ultra dégrade au lieu d'améliorer. Ça correspond exactement à ce que j'ai vu : la coupe emporte parfois un guard. Et la question de fond —la contrainte always-on nuit-elle au raisonnement sur des tâches dures (SWE-bench, Terminal-bench) ?— reste ouverte et sans données (16 👍, sans réponse).

Verdict

Coupe de code vs. ce qui est annoncé 57% · anunciado 54%

Moyenne de -57% sur mes 4 tâches vs. le -54% annoncé — reproduit le chiffre même sur un modèle fort et un grand dépôt. La claim de LOC tient.

Réutilise au lieu de réinventer (barreaux 2-5) 85%

A trouvé slugify dans 522k lignes, réutilisé les patterns du dépôt et la stdlib, jamais écrit plus. Il le fait bien — même si un baseline fort réutilise souvent déjà pareil.

La coupe préserve ce qui compte 45%

2 des 4 coupes ont enlevé quelque chose : persistance (T1), tests + types (T4), un guard de validation (T3). Sa propre règle « laisse UN check » a été sautée une fois.

Honnêteté du projet et de son benchmark 92%

Rare : a corrigé son propre chiffre gonflé, publié un bug de contamination interne, répondu aux critiques, montre où il ne gagne pas. Une référence sur comment mesurer.

Le gain dépend du modèle 40%

Le -94% vedette est un chiffre Haiku. Sur un modèle fort la coupe vient du scope et de la cérémonie, pas d'éviter la sur-construction. Ils l'admettent et le benchmark indépendant (Opus 4.8) descend à -44%.

Robustesse sur les edge-cases 60%

Leur benchmark et #236 : pas de perte générale de sécurité, mais #236 a vu un crash sur un edge-case et ici ponytail a sauté un guard de validation. « Écris moins » sans relecture peut couper le check.

ponytail est un bon réglage par défaut : il ne gonfle pas, réutilise bien, et son équipe mesure avec une honnêteté que presque personne n'a. Mais le titre « -54% » est un chiffre Haiku ; sur un modèle capable la coupe est modeste et, dans la moitié de mes tests, a emporté quelque chose qui valait la peine — la persistance d'un champ, une poignée de tests, un guard de validation. L'outil lui-même livre une commande /ponytail-review qui te rend une liste de suppression : il suppose que tu relis le diff. C'est exactement le juste milieu de ce site — utiliser les agents avec discernement, mesurer avant de croire, relire avant de faire confiance. Ni rejeter la paresse par réflexe, ni avaler le « -94% » sans regarder ce qui a été coupé.