Skip to content

Le runner d'agents ​

Une tâche cron sur le VPS lit les issues GitHub étiquetées agent:ready, demande à Claude de les implémenter, demande à un second Claude de relire le résultat, puis s'arrête. Il ne merge jamais. Un humain le fait.

Cette page explique comment il fonctionne et comment le piloter. Le script est la source de vérité — si cette page et ops/agent-runner/agent-runner.sh se contredisent, le script gagne et cette page est un bug. Le raisonnement derrière la liste d'outils autorisés vit à côté du script, dans ops/agent-runner/README.md.

État au 2026-07-30 ​

L'entrée cron est installée et le fichier PAUSE est en place, ce qui signifie que chaque tick sort immédiatement sans rien faire. Le script a été vérifié au shellcheck mais n'a jamais tourné de bout en bout sur un vrai ticket. Le premier run est un test, avec un humain qui lit le log en direct.

Le flux d'étiquettes ​

quelqu'un écrit en prose
        │
        ▼
  agent:spec ──── snag-spec ────▶ agent:ready ─────┐
                       │                          │
                       └──▶ agent:blocked         │  le runner le ramasse
                            (questions pour       │
                             un humain)           ▼
                                            agent:working
                                                  │
                    ┌─────────────────────────────┴──────────────┐
                    ▼                                            ▼
              PR ouverte                                 aucune PR ouverte
                    │                                   (l'agent a refusé)
              snag-review                                       │
                    │                                           ▼
                    ▼                                    agent:blocked
             agent:reviewed
                    │
                    ▼
            un humain merge

agent:working existe pour qu'un run mourant en plein ticket ne puisse pas amener le run suivant à reprendre le même ticket et à ouvrir une PR en double.

Un ticket qui finit en agent:blocked n'est pas un échec du système — c'est le système qui fonctionne. L'agent a trouvé quelque chose qu'il ne pouvait pas résoudre et l'a dit au lieu de deviner. Lisez son commentaire.

Un cycle, étape par étape ​

  1. Garde-fous. Sortir si PAUSE existe. Prendre un flock exclusif pour que deux runs ne se chevauchent jamais. Lire .env. Vérifier que gh s'authentifie, et échouer bruyamment sinon.
  2. Trouver du travail. Pour chaque dépôt de AGENT_REPOS, lister les issues ouvertes étiquetées agent:ready, les plus anciennes d'abord, plafonnées à AGENT_MAX_TICKETS_PER_RUN.
  3. Nettoyer le checkout. git reset --hard origin/main puis git clean -fd avant chaque ticket. Le runner ne présuppose rien de l'état laissé par un run précédent.
  4. Livrer. Re-étiqueter agent:ready → agent:working, puis lancer Claude en mode headless avec le skill snag-deliver sur cette issue.
  5. Retrouver la pull request. Chercher parmi les PR ouvertes celle dont le corps contient Closes #<issue>. C'est par cette chaîne que le runner sait que l'agent a réellement ouvert quelque chose, et c'est pourquoi snag-deliver l'exige.
  6. Relire. Lancer un second Claude avec le skill snag-review sur cette PR. Le relecteur est lancé sans Write et sans Edit — il ne peut pas corriger discrètement ce qu'il trouve, seulement le signaler.
  7. Étiqueter le résultat. agent:reviewed si une PR existe, sinon agent:blocked avec un commentaire portant l'identifiant du run.

Quels skills l'agent reçoit ​

Le runner fait un cd dans le checkout du dépôt avant d'invoquer Claude, donc Claude découvre le .claude/ propre à ce dépôt. Les skills sont par dépôt, pas globaux, et le prompt nomme celui qu'il veut au lieu de l'espérer :

Use the snag-deliver skill to implement GitHub issue #<n> in this repository
Use the snag-review skill to review pull request #<n>
Dépôt.claude/skills/
poc-tcg-collectorsnag-stack snag-spec snag-deliver snag-review
tcg-collector-backendles quatre mêmes, contenu différent
documentationaucun pour l'instant — voir Limites connues

Les deux jeux ne sont pas des copies. Le snag-deliver du backend porte l'étape obligatoire de câblage d'un nouveau fichier de test dans npm test (le backend n'a pas de glob ; un fichier de test non câblé fait échouer tests/testWiring.test.mjs, volontairement). Celui du mobile porte le contrat de palette et les zones interdites, android/ et ios/.

Le CLAUDE.md du dépôt et le ~/.claude/CLAUDE.md global de l'opérateur se chargent également, si bien que la discipline de branche — commits conventionnels, jamais de push direct sur main — s'applique à l'agent et pas seulement à un humain.

Où tout se trouve ​

Script/home/kael/skn-tcg/tcg-collector-backend/ops/agent-runner/agent-runner.sh
Configuration.env à côté, mode 600, gitignoré
Checkouts/home/kael/skn-tcg/<dépôt>
Verrous, coupe-circuit, logs/home/kael/.agent-runner/
Log d'un run/home/kael/.agent-runner/logs/<run-id>.log
Transcript Claude complet…/logs/<run-id>-deliver-<dépôt>-<issue>.json
Sortie de cron/home/kael/.agent-runner/cron.log

Le dossier d'état est hors de tout dépôt, volontairement : rien de ce que l'agent écrit ne doit jamais pouvoir devenir un candidat au git add.

L'espace de travail s'appelle toujours skn-tcg alors que l'organisation est désormais snag-tcg. C'est délibéré — dix bind mounts répartis sur cinq conteneurs actifs passent par ce chemin, dont le volume de données Postgres. Voir l'historique des renommages.

La planification ​

*/20 8-20 * * 1-5 /home/kael/skn-tcg/tcg-collector-backend/ops/agent-runner/agent-runner.sh >> /home/kael/.agent-runner/cron.log 2>&1

Toutes les vingt minutes, de 8h à 20h, du lundi au vendredi. Rien la nuit, parce qu'un run qui casse à 3h du matin reste là à être faux jusqu'au matin sans personne pour le remarquer.

Les huit binaires dont le script a besoin — node, npm, git, gh, claude, timeout, flock, jq — résolvent sous le PATH minimal de cron parce que tous les huit sont dans /usr/bin. C'est la panne classique du cron qui marche à la main et pas en planifié ; elle n'aura pas lieu ici.

Démarrer et arrêter ​

sh
touch /home/kael/.agent-runner/PAUSE     # tout run futur sort immédiatement
rm    /home/kael/.agent-runner/PAUSE     # reprise

Cela prend effet au tick suivant sans toucher au crontab, et cela survit à un redémarrage. Si quelque chose semble anormal, c'est le premier geste ; le diagnostic vient ensuite.

Pour observer un seul cycle à la main avant de faire confiance à la planification :

sh
cd /home/kael/skn-tcg/tcg-collector-backend/ops/agent-runner
AGENT_MAX_TICKETS_PER_RUN=1 ./agent-runner.sh

Sur un seul ticket jetable, dans un seul dépôt, avec un humain qui lit le log en direct. La panne que l'on découvre ainsi coûte une heure. Celle que l'on découvre par douze pull requests ouvertes un lundi matin coûte une journée.

Authentification ​

Le runner n'a pas besoin d'un token propre pour fonctionner. Deux voies :

  • GH_TOKEN rempli dans .env — un PAT fine-grained appartenant à l'organisation snag-tcg, limité aux trois dépôts, avec Contents, Pull requests et Issues en lecture/écriture, plus Checks et Commit statuses en lecture. Ces deux dernières permissions sont ce qui permet au runner de voir la CI ; sans elles l'API répond 403 et un build rouge peut se lire comme « aucun check en attente ».
  • GH_TOKEN vide — repli sur ce avec quoi gh est déjà authentifié sur la machine : un token OAuth créé par un login navigateur, stocké dans ~/.config/gh/hosts.yml en mode 600, aux scopes larges et sans expiration.

Le log du run dit laquelle des deux est en jeu, sur sa première ligne. Le repli est acceptable pour un tour à blanc surveillé et faux pour le cron, parce que ce token est une personne, avec les droits admin, et que le runner en hérite entièrement.

Le git push en HTTPS passe par le même identifiant dans les deux cas : le credential helper git de cette machine est !/usr/bin/gh auth git-credential, et il honore GH_TOKEN.

N'installez jamais le token du runner avec gh auth login. Un token fine-grained ne cohabite pas avec le token OAuth d'un compte, il le remplace — s'y connecter détruirait le gh interactif fonctionnel de la machine, et s'il s'avérait ensuite qu'une permission manque, vous déboguerez sans aucun accès.

Ce qui vous protège réellement ​

Soyons lucides : le script vous protège très peu. Les couches, de la plus forte à la plus faible.

CoucheCe qu'elle arrête
Un humain mergetout — et gh pr merge n'est dans aucune des deux allowlists, donc c'est le CLI qui l'impose, pas une convention
La liste d'outils autorisésforce push, suppression de branche, réécriture d'historique, lecture de secrets, réglages du dépôt, shell arbitraire
Le relecteur privé de Writeun relecteur qui corrigerait en silence ce qu'il devait signaler
Un token GitHub restreintles dégâts hors des trois dépôts
block-main-commit.shun commit qui atterrit sur main — mais c'est un hook git, donc ne lancez jamais l'agent avec --bare
AGENT_MAX_TICKETS_PER_RUNune nuit qui s'emballe. Cela borne le désordre, cela ne l'empêche pas
Le fichier PAUSEtout, immédiatement
Les règles de rayon d'action des skillsandroid/, ios/, migrations, workflows, montées de dépendances — faible : un prompt est une consigne, pas un bac à sable

Chaque entrée de l'allowlist est une sous-commande énumérée plutôt qu'un joker, parce que les jokers qui paraissent étroits ne le sont pas : Bash(gh *) inclut gh pr merge, gh secret set et gh repo delete. Avec ce joker, « il ne merge jamais » reposerait sur un prompt qui demande gentiment. Énuméré, cela repose sur le refus du CLI.

Limites connues ​

  • Le script n'a jamais tourné de bout en bout. Écrit et vérifié au shellcheck, pas exercé sur un vrai ticket.
  • Le quota Claude est partagé. Le VPS est connecté sur un plan Max 5x, que deux tickets par run ne mettent pas en tension — mais c'est le même quota qu'une session interactive ailleurs sur ce compte. Un rate limit inexpliqué en milieu d'après-midi est une raison de lire le crontab avant d'accuser autre chose.
  • Aucun plafond de coût. claude --max-budget-usd n'existe que pour les utilisateurs à clé API ; sur un abonnement il n'y a rien à plafonner. Le plafond de tickets et la fenêtre cron sont les seules bornes.
  • documentation n'a aucun skill. Sans effet aujourd'hui puisqu'il n'est pas dans AGENT_REPOS, mais l'y ajouter sans avoir d'abord écrit son .claude/skills/ lancerait un agent sans garde-fous et échouerait au premier prompt.
  • Le runner ne vérifie pas qu'un ticket appartient au dépôt où il a été déposé. C'est à snag-spec d'aiguiller. Un ticket mobile sur le backend sera refusé, mais seulement après avoir brûlé un run.
  • Les skills personnels de l'opérateur sont visibles aussi.~/.claude/skills/ est sur le chemin et Skill est dans l'allowlist, donc l'agent peut invoquer tdd, prototype et les autres. Risque faible, un skill ne s'activant que nommé et le prompt en nommant un — mais à surveiller sur les premiers runs.