Pendant longtemps, mon setup d'agents avait un angle mort.
Builder recevait une issue, codait, créait une PR. Moi je reviewais le lendemain matin. Ça marchait. Mais je passais encore 20-30 minutes par PR à vérifier que rien n'avait déraillé : API inventée, import cassé, logique qui court-circuite un edge case.
J'ai résolu ça avec un pattern simple : le Swarm. Un agent qui code (CODER), un agent qui review (REVIEWER). Deux sessions Claude indépendantes, même issue, handoff propre.
Ce n'est pas révolutionnaire sur le papier. Mais les détails d'implémentation changent tout.
Pourquoi un agent seul ne suffit pas
Un agent isolé a un problème structurel : il ne peut pas se critiquer lui-même de façon fiable.
Quand Builder écrit du code, il est dans un état mental d'"exécution". Il optimise pour que ça marche. Il ne va pas spontanément chercher pourquoi ça pourrait casser. C'est comme demander à quelqu'un de relire son propre CV une heure après l'avoir écrit — les fautes passent.
Ce que j'observais :
- ~10% d'hallucinations API : Builder appelait des fonctions qui n'existaient pas dans le codebase (souvent des méthodes inventées à partir du nom du fichier)
- Régressions silencieuses : des changements qui ne cassaient pas le build mais qui changeaient un comportement attendu ailleurs
- Oubli des contraintes : le brief disait "pas de CSS custom", l'implémentation en ajoutait quand même
Le build passait. La PR semblait propre. Mais des problèmes s'accumulaient.
La solution n'était pas un meilleur modèle ou un meilleur brief. C'était un second regard structurellement indépendant.
L'architecture Swarm
Issue Paperclip │ ▼ [Jarvice] — lit l'issue, crée le worktree, prépare le brief │ ▼ [CODER] — implémente dans le worktree isolé │ ▼ (résultat : commit + diff + rapport) [REVIEWER] — lit le diff, vérifie, critique, approuve ou bloque │ ▼ [Jarvice] — crée la PR si REVIEWER ✅, nettoie le worktree
Le point clé : CODER et REVIEWER ne partagent aucun contexte conversationnel. Le REVIEWER reçoit uniquement le diff git, le rapport du CODER, et les critères d'acceptation de l'issue. Il ne "sait pas" que c'est lui qui a codé. Il n'a aucun biais de confirmation.
Le worktree git : la fondation technique
Sans isolation filesystem, deux agents sur le même repo créent du chaos. La solution c'est git worktree — une feature native git qui permet d'avoir le même repo checkouté sur deux branches dans deux répertoires séparés, sans interférence.
# Dans Jarvice, avant de spawner le CODER ISSUE_ID="187" BRANCH="fix/issue-${ISSUE_ID}" WORKTREE_PATH="/tmp/ap-${ISSUE_ID}" git worktree add "${WORKTREE_PATH}" -b "${BRANCH}"
Le CODER travaille dans /tmp/ap-187. La branche main d'AutoPrestige reste intacte dans /Users/valentin/Documents/Dev/autoPrestige. Aucun conflit possible.
Quand tout est terminé :
git worktree remove "${WORKTREE_PATH}"
C'est trois commandes. Mais c'est ce qui rend le pattern Swarm scalable — je peux lancer plusieurs issues en parallèle, chacune dans son worktree, sans qu'elles se marchent dessus.
Le brief CODER : la variable qui change tout
Le brief n'est pas optionnel. C'est la spec que le CODER reçoit, et sa qualité détermine 80% du résultat.
Voici le format que j'ai convergé après 3 semaines d'itérations :
# Brief CODER — Issue #187 ## Contexte AutoPrestige est une marketplace de véhicules premium. Stack : Next.js 16, TypeScript strict, Prisma + PostgreSQL, Tailwind CSS v4. Repo : /tmp/ap-187 (worktree isolé, branche fix/issue-187) ## Ce qu'il faut faire 1. Migrer le rate limiter actuel (in-memory) vers Upstash Redis 2. Utiliser la variable d'env UPSTASH_REDIS_URL (déjà définie en prod) 3. Wrapper la logique dans src/lib/rate-limit.ts (ce fichier existe) 4. Mettre à jour les 2 routes qui l'utilisent : POST /api/auth/login et POST /api/contact ## Règles ABSOLUES - TypeScript strict : pas de `any` - Pas de nouvelle dépendance sans vérifier package.json d'abord - Ne pas toucher aux fichiers hors du périmètre ci-dessus - Committer avec le format : fix: migrate rate limiter to Upstash Redis (#187) ## Critères d'acceptation - [ ] pnpm run build passe sans erreur - [ ] pnpm run lint passe sans warning - [ ] Le rate limiter fonctionne par IP (clé = "rl:${ip}") - [ ] Les 2 routes retournent 429 si limite dépassée ## Ce que tu dois retourner Un rapport Markdown avec : - Ce que tu as fait (liste des fichiers modifiés) - Les décisions non triviales prises - Les points d'attention pour le reviewer - Hash du dernier commit
La section "Règles ABSOLUES" est celle que j'ai ajoutée après avoir eu des surprises. Un agent sans contrainte explicite interprète "faire la migration" avec beaucoup de créativité.
Le brief REVIEWER : l'art du regard neuf
Le REVIEWER reçoit un contexte différent. Il ne lit pas le brief du CODER. Il reçoit :
- Les critères d'acceptation de l'issue originale
- Le diff git (
git diff main...fix/issue-187) - Le rapport du CODER
- Sa mission : valider ou bloquer
# Brief REVIEWER — Issue #187 ## Ta mission Tu es un développeur senior qui review une PR. Sois critique. Sois précis. Tu n'as aucune loyauté envers le code que tu vas lire — ton job est de trouver les problèmes. ## Critères d'acceptation (source : issue #187) - Rate limiter migré vers Upstash Redis - Fonction par IP avec clé "rl:${ip}" - Routes /api/auth/login et /api/contact retournent 429 si limite dépassée - Build et lint propres ## Rapport du CODER [rapport inséré ici] ## Diff git à reviewer [sortie de : git diff main...fix/issue-187] ## Ce que tu dois vérifier 1. Tous les critères d'acceptation sont satisfaits (test unitairement dans ta tête) 2. Pas de régression évidente sur les fichiers touchés 3. TypeScript strict respecté (pas de `any` introduit) 4. Pas de secret hardcodé (API keys, passwords) 5. La logique de rate limiting est correcte (edge cases : IP manquante ? limites ?) ## Format de réponse **STATUT : ✅ APPROUVÉ** ou **STATUT : ❌ BLOQUÉ** Si BLOQUÉ : liste précise des problèmes avec références aux lignes du diff. Si APPROUVÉ : résumé de 2-3 lignes de ce qui a été fait.
Le mot "loyauté" dans le brief n'est pas un détail. Sans lui, les LLM ont tendance à valider par défaut — ils cherchent à être utiles, et valider semble utile. Ce mot change l'état d'esprit de l'agent.
Ce que le REVIEWER a trouvé que j'aurais raté
Trois exemples concrets des 6 dernières semaines :
Issue #192 — conversion Float → Int : Builder avait utilisé Math.round() pour la conversion de prix. Le REVIEWER a flagué que sur des montants > 999999, JavaScript float perd de la précision. Le bon fix : Math.trunc() avec validation de range en amont.
Issue #201 — nouveau formulaire contact : Builder avait oublié d'ajouter le champ honeypot pour le spam. Le brief original ne le mentionnait pas. Le REVIEWER l'a demandé de lui-même en lisant le reste du codebase.
Issue #178 — refacto layout admin : Le CODER avait modifié un composant partagé entre l'admin et la partie publique. Le REVIEWER a détecté que ça cassait un comportement sur une page hors périmètre. BLOQUÉ. Correction en 10 minutes.
Dans les trois cas, le build passait. Le lint passait. Sans le REVIEWER, ces problèmes auraient atterri en prod.
Quand utiliser le pattern Swarm vs coder directement
Le Swarm a un coût : environ 2x plus de tokens, et un temps d'exécution plus long (les deux sessions sont séquentielles, pas parallèles).
J'applique une règle simple :
| Type de changement | Approche |
|---|---|
| Fix trivial (1 fichier, < 20 lignes) | Code direct, pas de Swarm |
| Feature modérée (2-5 fichiers) | Swarm |
| Refactoring / migration | Swarm obligatoire |
| Nouveau composant ou page | Swarm |
| Config / documentation seulement | Code direct |
Le seuil "2-5 fichiers" est empirique. En dessous, le coût du Swarm dépasse le bénéfice. Au-dessus, le REVIEWER devient indispensable.
L'implémentation dans OpenClaw
Dans mon setup, Jarvice orchestre le Swarm via des sous-agents spawned. Le workflow simplifié :
// Jarvice reçoit une issue Paperclip // 1. Crée le worktree await exec(`git worktree add ${worktreePath} -b ${branch}`) // 2. Spawne le CODER (Claude Code, background) const coderResult = await spawnSubagent({ task: buildCoderBrief(issue, worktreePath), runtime: 'acp', agentId: 'claude-code', cwd: worktreePath }) // 3. Lit le diff const diff = await exec(`git diff main...${branch}`, { cwd: worktreePath }) // 4. Spawne le REVIEWER const reviewerResult = await spawnSubagent({ task: buildReviewerBrief(issue, coderResult.report, diff), runtime: 'acp', agentId: 'claude-code', cwd: repoPath // reviewer travaille sur le repo principal (lecture seule) }) // 5. Décision finale if (reviewerResult.status === 'APPROVED') { await createPR(branch, issue) await updatePaperclipIssue(issue.id, 'review') } else { await commentPaperclip(issue.id, `REVIEWER a bloqué : ${reviewerResult.issues}`) }
La magie est dans l'isolation des contextes. Le REVIEWER ne voit jamais la session du CODER — il est spawné dans un processus entièrement neuf.
Ce qui reste imparfait
Honnêteté totale.
Le REVIEWER hallucine parfois dans l'autre sens : il bloque sur des faux positifs. Il m'est arrivé d'avoir un REVIEWEUR qui marquait "BLOQUÉ" pour un pattern TypeScript parfaitement valide qu'il ne reconnaissait pas. J'ai dû intervenir manuellement pour débloquer.
Solution en cours : un fichier CODEBASE-PATTERNS.md à la racine de chaque projet qui documente les patterns non-standard utilisés. Le REVIEWER le lit en amont.
L'autre limitation : les sessions CODER et REVIEWER sont séquentielles. Sur une nuit avec 4 issues, ça prend du temps. Je cherche à paralléliser les Swarms entre eux (chaque Swarm est indépendant) — techniquement faisable, mais ça multiplie les risques de conflits git sur les fichiers partagés.
Le résultat net
Depuis que j'ai mis le Swarm en place :
- 0 bug majeur passé en prod directement depuis un agent (sur 34 issues traitées)
- Temps de review humaine par PR : de 25 min à 8 min en moyenne
- Taux de PRs bloquées par le REVIEWER : ~18% (6 sur 34)
Ces 18% de blocages, c'est le chiffre qui me convainc. Sans le REVIEWER, ces 6 issues auraient atterri en prod avec des problèmes réels.
Le Swarm n'est pas une garantie. C'est une couche de filet supplémentaire. Et dans un système qui tourne en autonomie la nuit, chaque filet compte.
La prochaine étape : faire que le REVIEWER puisse lui-même spawner un PATCHER quand il détecte quelque chose de trivial — corriger l'erreur sans renvoyer à zéro. La boucle se referme complètement.
Le code des briefs CODER/REVIEWER est disponible sur le repo GitHub →