ponytail : moins de code, du meilleur code ?
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.
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 :
<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 :
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 :
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 :
Endpoint + un modèle de réponse typé (OwnedItemsCount) + 3 tests : compte 0→3, ne compte pas les items d'autrui, exige l'auth.
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 :
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
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é.