Accéder au contenu principal

14min.

D’un ADE à un orchestrator : la construction de PABLO

This blog post is also available in 🇬🇧 English: From an ADE to an Orchestrator: Building PABLO.

Mes agents sont devenus plus rapides. Mon cerveau, non.

Section intitulée ou-nous-en-etionsOù nous en étions

Dans mon article précédent, je décrivais mon environnement de travail : un Agent Development Environment, non pas un IDE avec un panneau de conversation greffé, mais un outil construit autour de tâches, de worktrees et d’agents. Je prends une issue, l’ADE ouvre un worktree isolé avec un agent déjà en cours d’exécution, et je dirige cet agent au lieu de taper du code. Par-dessus se trouvent les agents que j’ai affinés pendant des mois, tous en lecture seule par construction ; build est le seul autorisé à modifier le code.

Les gains de ces agents se concentrent en un point : la compréhension. À mon travail, nous n’avons pas d’équipes spécialisées, donc n’importe qui peut se voir confier n’importe quelle tâche : un bug de synchronisation de stock le matin, une fonctionnalité dans une partie du code que je n’ai jamais ouvert l’après-midi, le prochain ticket dans l’application retail. Chaque tâche pourrait être un domaine différent avec ses propres règles métier, et je bascule entre elles trois ou quatre fois par jour. L’agent task-analyst lit le ticket, ses parents et le code associé, puis me livre l’ensemble en une seule lecture.

Comprendre un ticket est une chose, mais une fonctionnalité n’est pas terminée quand la branche est poussée. Entre le code et la production se trouve un second métier fait d’attente et de vérifications : le statut de la CI, les reviews, la QA. Tout cela vit dans GitHub et Jira, en dehors de mes worktrees. J’avais automatisé la réflexion et gardé l’attente.

Cet article porte sur l’outil que j’ai construit pour combler ce fossé : PABLO, une application Symfony. En son cœur se trouve une machine à états, quelque chose que les développeurs PHP livrent depuis une décennie sous un nom moins à la mode que celui qu’utilise l’écosystème IA aujourd’hui.

Section intitulée les-corvees-que-personne-n-a-automatiseesLes corvées que personne n’a automatisées

Voici à quoi ressemblait réellement mon matin : avant d’écrire la moindre ligne, je devais traiter une checklist mentale sur 5 ou 6 pull requests ouvertes et deux ou trois projets.

  • La CI est-elle verte ? L’ADE n’affiche le statut que pour la PR du worktree courant ; les autres vivent dans des onglets de navigateur.
  • Quelqu’un les a-t-il reviewées, et est-il temps de redemander sans devenir la personne qui demande toutes les 90 minutes ?
  • La QA les a-t-elle planifiées, ou cassées ?
  • Du feedback est-il arrivé sur le ticket ? Le trouver, recréer un worktree pour une branche supprimée la semaine dernière.
  • La CI est-elle rouge ? Alors tout ce que les agents font pour moi est inutile tant que je ne relance pas les choses moi-même : remarquer l’échec, créer un worktree, choisir l’agent, le lancer, attendre.

Rien de tout cela n’est difficile, et c’est exactement le problème : chaque point coûte un changement de contexte, la chose que j’avais passée un an à éliminer. Ce ne sont jamais les minutes qui faisaient mal ; ce qui faisait mal, c’était l’entrelacement : vérifier la CI, revenir à une tâche, se rappeler que personne n’a reviewé, revenir encore.

Mes agents avaient éliminé le bruit, rendant le travail facile à comprendre. Mais le coût du suivi de toutes ces tâches restait le mien. Une douzaine de pièces mobiles à travers trois outils, et dans ce système, quelque chose doit poller. C’était moi.

Section intitulée pabloPABLO

Alors j’ai fait : PABLO, pour Personal Assistant for Boring Logic & Operations. L’acronyme est venu après le nom, mais « boring » (ennuyeux) est le mot honnête dedans. Rien de ce que fait PABLO n’est malin : il regarde les pull requests, lit les dates, les compare avec 10 minutes auparavant, et conclut que quelque chose a changé (ou pas).

L’ADE reste l’endroit où je travaille, et PABLO reste un niveau au-dessus, répondant en boucle à une seule question : étant donné l’état de cette pull request, y a-t-il quelque chose qu’un agent devrait faire, et si oui, lequel ? Quand la réponse est oui, PABLO lance automatiquement cet agent via le binaire de l’ADE, dans le bon worktree, exactement comme je l’aurais fait.

Sous le capot se trouve une application console Symfony avec trois points d’entrée :

  • un binaire pablo ;
  • la console Symfony ;
  • un petit dashboard.

Il y a une exception à ce tableau : /pablo-commit-and-pr est une commande OpenCode, pas une commande PABLO. L’agent écrit le message de commit et la description de la pull request (la seule partie du flux où un agent bat du simple code), puis rend la tâche à PABLO, qui déplace la tâche vers l’état draft et prend le relais.

Et enfin, un scheduler en arrière-plan (systemd sur Linux ou launchd sur macOS) met à jour les informations sur les pull requests toutes les cinq minutes.

Cette séparation compte plus qu’il n’y paraît. PABLO ne demande jamais qu’à lancer un agent dans un worktree et à être prévenu quand le run se termine, donc tout ce que PABLO sait de l’ADE tient derrière une seule interface : AgentLauncherInterface. L’interface rend l’ADE remplaçable : si un meilleur arrive le mois prochain, je réécris une classe. Le remplacement a déjà eu lieu une fois : je travaillais avec Orca avant, et je travaille avec OpenChamber aujourd’hui. Un couplage va plus loin : quand l’ADE refuse un worktree, la porte de secours est un opencode run headless. PABLO ne dépend vraiment que d’OpenCode.

Pourquoi PHP pour un daemon en arrière-plan en 2026 ? C’est le langage dans lequel je pense le plus vite, et le seul utilisateur, c’est moi.

Une dernière décision : PABLO ne stocke aucun token et ne parle à aucune API. Tout passe par des CLIs déjà authentifiés sur ma machine (gh, acli, linear, orca, openchamber), donc il n’y a rien à faire tourner ou à fuiter. Quand quelque chose manque, pablo system:doctor le dit, avant qu’un cron n’échoue silencieusement à 3 heures du matin.

L’intelligence a toujours été dans les agents. Ce qui manquait, c’était quelque chose d’assez bête pour tourner toutes les cinq minutes pour toujours sans s’ennuyer.

Section intitulée graph-engineering-ou-une-machine-a-etats-avec-un-meilleur-nomGraph engineering, ou : une machine à états avec un meilleur nom

Si vous avez lu sur les agents IA récemment, vous avez vu le terme « graph engineering » : modéliser le travail en nœuds et en arêtes au lieu d’un seul énorme prompt. Enlevez le vocabulaire à la mode, et ce n’est qu’une machine à états. Parce que le développement logiciel est brouillon, la machine à états de PABLO doit être flexible. Plusieurs états doivent facilement retomber sur draft, et une commande manuelle peut extraire une tâche d’un état à tout moment. Les frameworks de machines à états lourds exigent de déclarer à l’avance chaque transition autorisée et bloquent tout le reste. C’est trop rigide pour mon workflow, alors j’ai sauté les frameworks et construit un moteur simple : des états, des transitions, et une règle souple sur ce qui est autorisé depuis où.

Dans la plupart des frameworks d’agents, les nœuds sont des étapes de raisonnement (plan, search, summarize, critique), et le graphe vit à l’intérieur de la tâche. Dans PABLO, les nœuds sont les états qu’une pull request traverse dans une vraie équipe : drafted, CI en échec, attente de review, changes requested, attente de QA. Le graphe n’est pas l’agent : c’est le processus dans lequel je travaille déjà. C’est ce qui garde le graphe stable après la prochaine sortie de modèle, les prompts et les modèles vivant à l’intérieur des nœuds.

Donc la machine à états est une table. Littéralement une table, dans une classe :

State::Draft->value => ['emoji' => '📝', 'label' => 'draft',
                        'onEnter' => [self::class, 'enterDraft'], 'polled' => true],
State::CiRed->value => ['emoji' => '🔴', 'label' => 'ci-red',
                        'onEnter' => [self::class, 'enterCiRed'], 'polled' => true],

Chaque ligne de la table déclare comment afficher l’état, ce qui s’exécute à l’entrée, et si le poller le surveille. À côté de la première table, une seconde dit ce que chaque état peut vérifier :

public const POLL_CHECKS = [
    State::Draft->value         => ['checkCiRed', 'checkCiGreen'],
    State::CiRed->value         => ['checkCiGreen'],
    State::ReadyToReview->value => ['checkCiRed', 'checkReviews'],
    State::WaitingReview->value => ['checkCiRed', 'checkReviews'],
    State::NeedsTesting->value  => ['checkFailureSignal'],
];

C’est tout le moteur. Ajouter une étape est une ligne et une méthode, pas un refactoring. Le handler on-enter unique est partagé par le poller, les commandes et les overrides manuels, donc les transitions ne divergent jamais. Même idée pour les trackers : une interface de provider, et la machine à états n’a aucune idée de quel tracker elle parle.

Section intitulée les-etats-et-ce-qui-les-reveilleLes états, et ce qui les réveille

Il y a neuf états. Pas de todo, pas d’init, exprès : une issue que je n’ai pas commencée n’a pas de tâche et apparaît simplement dans la liste de ce qui m’est assigné. Entrer dans in-progress crée la tâche, la branche, le worktree et le premier run d’agent ; un merge ferme la tâche depuis n’importe où et supprime le worktree.

État Entré par Ce qui se passe à l’entrée
🔨 in-progress pablo task:start <issue-url> crée la branche et le worktree, lance task-analyst une fois, plus le script de démarrage du projet s’il en a un
🥱 waiting pablo task:waiting rien, et le polling s’arrête. Revenir en arrière restaure l’état précédent
📝 draft /pablo-commit-and-pr commit, push, draft pull request ouverte
🔴 ci-red poller : la CI échoue lance ci-analyst sur les checks en échec et leurs logs
👀 ready-to-review poller : la CI est verte marque la pull request prête sur GitHub, puis bascule immédiatement vers l’état suivant
👀 waiting-review atteint depuis le précédent rien, c’est ici qu’une tâche attend des humains
🧪 needs-testing poller : une review approbatrice est arrivée enregistre le timestamp qui devient la baseline QA
🔁 request-changes poller : changes requested repasse la pull request en draft, lance pr-feedback
🚨 testing-failed poller : le signal d’échec QA s’est déclenché repasse la pull request en draft, lance task-feedback

La forme de la chose, en graphe :

Graph Mermaid

Regardez les flèches, pas les boîtes : chaque flèche vers draft est une commande que j’ai tapée ; chaque autre flèche est le poller. Je décide quand le travail quitte mes mains ; la machine gère le reste.

Trois détails ont demandé bien plus d’itérations que prévu.

Le premier : ce qui compte comme une review. Mes propres reviews sont exclues : répondre à un commentaire sur ma propre pull request crée une review à mon nom, ce qui me renverrait sinon en draft juste pour avoir répondu à une question. Les reviews de bots sont ignorées sauf whitelist. Seules les reviews postérieures au dernier événement timeline ready_for_review de GitHub comptent (un vrai timestamp, contrairement au verdict du summary), donc une approbation périmée ne peut pas fuiter dans ce tour. La dernière review par reviewer gagne, et tout ce qui n’est pas une approbation bat toutes les approbations : si quelqu’un a pris la peine d’écrire, je lis le feedback avant que la QA ne le fasse.

Le deuxième : exactement un seul chemin de retour vers draft, et c’est une commande. Pas de détection de push : un git push brut ne déplace rien. Cela semble restrictif jusqu’à ce qu’on considère que je pousse des dizaines de fois par jour, souvent juste pour lancer la CI. Une commande explicite est un signal. Un push est du bruit.

Le troisième : ce que j’ai délibérément choisi de ne pas traiter comme des événements. CI pending ne déclenche rien. CI rouge dans needs-testing ne déclenche rien non plus : un build cassé apparaîtra au moment du merge, et interrompre la QA n’aide personne. La moitié de la conception d’une machine à états, c’est choisir ce qui n’est pas un événement, et cette moitié ne reçoit presque aucune attention.

Dashboard web PABLO

Section intitulée ce-que-vert-veut-reellement-direCe que « vert » veut réellement dire

Deux de ces transitions pendent à une question : la CI est-elle verte ? Cela ressemble à un fait qu’on vérifie. Ça n’en est pas un.

Ma première version : rouge en échec, vert quand tout passe, en attente sinon. Cette règle fonctionnait partout sauf sur le projet où j’avais le plus besoin de PABLO.

Ce projet tourne sous CircleCI, qui publie ses deployment gates sur GitHub comme status contexts : deploy-code-approval-ppr, deploy-code-approval-qlf. Ce sont des gates manuelles sur chaque pipeline, y compris les branches de feature où personne ne clique dessus (nous ne déployons pas en qualification à chaque commit). Elles restent à PENDING pour toujours, ou arrivent en ACTION_REQUIRED, que ma règle comptait comme échec, donc « La CI est-elle verte ? » n’était jamais oui.

Le correctif est une ligne de configuration dans PABLO :

ci:
  ignore_checks: ["approval"]

Tout check dont le nom, le workflow ou le status context contient cette sous-chaîne est retiré de la question entièrement : il ne peut ni rendre le verdict rouge, ni le retenir en attente pour toujours. Deux gates, un mot.

Cette unique clé de configuration m’a forcé à réaliser quelque chose : une CI verte est une règle, pas un fait. Ce que GitHub appelle un « CI check » est en réalité un tas bruyant de tests unitaires, de gates de déploiement manuelles et de bots bavards. Comme aucun outil ne peut deviner magiquement lesquels comptent réellement pour une branche donnée, PABLO pousse cette décision vers le fichier YAML du projet. Pour garder le moteur simple, j’ai ajouté deux autres règles brutales : un dépôt avec zéro check est considéré comme vert, et le matching par sous-chaîne reste volontairement bête.

Gates d'approbation GitHub

Section intitulée la-seule-exception-a-mon-propre-dogmeLa seule exception à mon propre dogme

Dans mon article précédent, j’ai fait un point sur le fait que mes agents sont en lecture seule, pas par instruction mais par construction : le frontmatter (voir mon article précédent pour comprendre ce qu’est un frontmatter) refuse edit et write. Les quatre analystes de PABLO suivent la même règle : modèle pas cher et rapide, interdit en écriture.

Mais il y a cinq agents. Le cinquième agent est rebase-conflict-resolver, le seul endroit où j’ai cassé ma propre règle exprès. Toutes les douze heures, un job de sync rebase chaque worktree de mes tâche sur la branche primaire. En cas de conflit, git abandonne et réinitialise le worktree, et un agent est lancé pour refaire le rebase et résoudre chaque conflit. Son frontmatter montre le coût : edit: allow, write: allow, git push*: allow.

Pourquoi le laisser entrer ? La sync est un dry run par défaut. Une clé de configuration transforme le rapport en action : auto_apply: true, posé seulement là où je suis sur que j’ai le moins de risques possibles.

Son rayon d’action est borné comme tout le reste. Il peut écrire du code et pousser, mais toute commande agissant sur le processus plutôt que sur le code (merging, approving, commenting) est refusée. Il peut réécrire ma branche ; il ne peut ni l’approuver, ni la merger, ni parler en mon nom. La permission dangereuse n’a jamais été edit ; c’était agir en tant que moi devant d’autres personnes. Deux garde-fous de plus : une branche sale (fichiers non commités restants) n’est jamais rebasée, et quand l’agent prend le relais d’un rebase, PABLO affiche une commande claire et copiable pour voir la session de l’agent dans OpenCode.

Git fait le travail de sécurité restant. Le push est toujours --force-with-lease, jamais un --force nu. Si j’ai poussé moi-même pendant que le cron tournait, la vérification de lease échoue, le worktree revient en arrière, et le job réessaie douze heures plus tard. Mon push gagne toujours. Quand un conflit nécessite vraiment un humain, l’agent affiche PABLO_CONFLICT_UNRESOLVABLE et ne pousse rien.

La partie honnête : cet agent peut faire des dégâts, contrairement aux quatre autres. J’accepte ce risque parce que tout ce qu’il écrit atterrit dans une pull request que je review avant le merge. C’est mon propre code. En échange, je n’ai pas eu de pull request en conflit depuis un moment.

Reporting rebase web PABLO

Section intitulée tout-ne-merite-pas-un-llmTout ne mérite pas un LLM

Dans mon article précédent, je décrivais deux de mes commandes d’agent préférées : des prompts quotidiens qui appelaient le CLI gh, lisaient mes pull requests ouvertes, et utilisaient un LLM pour écrire un message Slack demandant à l’équipe des reviews et de la QA. Ça ressemblait à de la magie à l’époque. Aucune des deux n’a survécu. Elles sont maintenant simplement pablo show:prs : un script PHP lisant depuis le cache du poller, affichant exactement le même markdown prêt pour Slack avec une file de review, un séparateur, et une file de QA. Aucun LLM impliqué.

Le même instinct a façonné le dashboard web, rendu depuis le cache du poller, et cela mène à quelque chose à quoi je ne m’attendais pas. Un outil construit pour orchestrer des agents IA s’avère être des timestamps, des file locks, du CLI parsé et une table de transitions autorisées. L’intelligence est dans cinq fichiers markdown ; tout le reste est de la plomberie sans éclat qui décide quand ouvrir le robinet.

Section intitulée ce-qui-a-reellement-changeCe qui a réellement changé

La différence la plus claire : je n’ai pas vérifié moins, j’ai arrêté de vérifier moi-même.

Plus de tour de mes pull requests pour des pipelines rouges, plus de chasse aux reviews. Une liste, pablo tasks ou le dashboard web, me dit ce qui m’attend ; tout le reste est le tour de quelqu’un d’autre.

Le cas de la CI à lui seul a valu la construction de PABLO. La CI passe rouge à onze heures du soir, le poller le remarque en moins de dix minutes et lance ci-analyst dans le bon worktree ; au matin, le diagnostic attend. L’analyse était déjà automatisée par mes agents ; ce qui a changé, c’est que le fait de remarquer et l’attente sont automatisés aussi.

Une tâche complète se passe maintenant ainsi : pablo task:start sur un lien Jira, travailler avec l’agent, /pablo-commit-and-pr, et arrêter d’y penser.

pablo task:start https://acme.atlassian.net/browse/XXX-123

Section intitulée le-temps-de-cerveau-est-la-ressourceLe temps de cerveau est la ressource

Le coût caché du développement moderne, ce ne sont pas les trois minutes que prend la vérification d’un pipeline CI ; c’est la taxe cognitive de garder cet onglet ouvert à l’arrière de votre tête. Le deep work exige de longues périodes d’attention ininterrompues. Quand vous le fracturez avec un ping-pong administratif : rafraîchir GitHub, courir après des approbations, se demander si la QA a pris votre ticket. Vous ne perdez pas seulement du temps, vous brûlez l’énergie mentale exacte dont vous avez besoin pour résoudre des problèmes difficiles. Je n’ai pas construit PABLO juste pour taper moins de commandes bash, ni pour automatiser pour automatiser. Je l’ai construit pour qu’il serve de bouclier cognitif. En déléguant la bureaucratie de la mise en production à une boucle PHP bête et inlassable, j’ai cessé d’être un routeur humain pour mes propres tâches. Je peux de nouveau être simplement un ingénieur.


Section intitulée pablo-est-publicPABLO est public

PABLO est public : github.com/korbeil/pablo. Il est construit pour exactement un utilisateur, le workflow de mon équipe, mes CLIs, ma tolérance à l’attente ; rien de tout cela n’est universel. Le publier revient à tendre à quelqu’un un fichier de config bien tenu : quelque chose à lire, à emprunter et à casser. La machine à états est une table précisément pour que la vôtre remplace la mienne sans combat.

Si vous voulez l’essayer, la version courte :

git clone https://github.com/korbeil/pablo && cd pablo
./bin/install.sh        # deps, liens agents/commandes, démarre le scheduler,
                        # finit avec pablo system:doctor
pablo system:doctor     # quel CLI manque ou est déconnecté
pablo project:new       # ajouter un projet ; ou déposer un YAML dans ~/.pablo/projects/

Vous avez besoin de PHP 8.4+ et des CLIs (gh, acli, linear, opencode) chacun déjà authentifié. Puis pablo task:start <issue-url> sur quelque chose de petit, /pablo-commit-and-pr, et arrêtez d’y penser. Si vous préférez un dashboard web au CLI, pablo web le sert sur le port 8321.

Démarrage PABLO

Commentaires et discussions

Nos articles sur le même sujet

Nos formations sur ce sujet

Notre expertise est aussi disponible sous forme de formations professionnelles !

Voir toutes nos formations