DEV Community

Cover image for Pourquoi osFoundry oblige Claude Code à préciser le commit quand il demande de relancer un check GitNest
BBT for BBT Français

Posted on

Pourquoi osFoundry oblige Claude Code à préciser le commit quand il demande de relancer un check GitNest

Voici l'outil qu'appelle Claude Code quand il veut relancer un check GitNest en échec, tel que l'endpoint MCP en production d'osFoundry le liste dans tools/list (pas besoin de token pour le catalogue) :

{
  "name": "propose_gitnest_check_rerun",
  "description": "Ask the repository owner to retry one finished check on a specific composed change-set version. Approval rechecks repository access, version, commit and check state.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "repository_id": {"type": "string"},
      "change_set_id": {"type": "string"},
      "check_name": {"type": "string", "maxLength": 200},
      "expected_change_set_version": {"type": "integer", "minimum": 1},
      "expected_composition_oid": {"type": "string"},
      "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128}
    },
    "required": ["repository_id", "change_set_id", "check_name",
                 "expected_change_set_version", "expected_composition_oid", "idempotency_key"],
    "additionalProperties": false
  },
  "annotations": {"readOnlyHint": false, "openWorldHint": false, "destructiveHint": true},
  "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]
}
Enter fullscreen mode Exit fullscreen mode

Son pendant en lecture prend deux arguments : list_gitnest_checks {repository_id, change_set_id}, "bounded check status for the current composed commit of a change set". Deux autres lectures alimentent la décision : list_gitnest_saved_results (limit 1–50) et read_gitnest_work (kind vaut tasks, changes ou runs). Les trois sont osfoundry:read avec readOnlyHint: true.

Six champs obligatoires pour relancer un seul check, c'est beaucoup. Un bouton de CI a besoin d'un ID de job. Les deux en plus, une version et un commit, sont le sujet de cet article. Aucun binaire claude n'est installé sur ce Mac, donc c'est checks_sim.py qui joue le client. Le code et sa sortie complète sont dans code/.

Que veut dire « vert » quand un agent lit les checks ?

Avant de proposer quoi que ce soit, l'agent doit lire correctement la liste des checks. L'UI GitNest d'osFoundry résume les check runs d'un commit avec une petite fonction pure (summarise dans check-runs.ts). L'ordre de ses tests, c'est la règle :

  1. Aucun run, c'est none, jamais passed.
  2. Le moindre failure, timed_out ou cancelled donne failed.
  3. Tout ce qui n'est pas encore success laisse l'état à running, y compris un statut que le client ne reconnaît pas.
  4. Seulement ensuite, c'est passed.

Le script porte cette règle et la compare à deux raccourcis qu'un modèle écrit de lui-même :

== verdict ==
case             summarise          no-failure  ratio 
no checks        none               green       green 
all passed       passed 2           green       green 
one queued       running 1/2        green       red   
one failed       failed: e2e        red         red   
one timed out    failed: e2e        green       red   
one cancelled    failed: e2e        green       red   
unknown status   running 1/2        green       red   
shortcut wrong: no-failure 5/7, ratio 1/7
Enter fullscreen mode Exit fullscreen mode

« Rien n'a échoué, donc c'est vert » se trompe dans 5 cas sur 7. Il déclare vert un commit jamais vérifié, un check en file d'attente et un timeout. Le test par ratio (passed == total) s'en sort mieux, mais avec zéro check, 0 == 0 et il dit vert lui aussi. C'est justement le cas que les commentaires du code GitNest qualifient de plus trompeur, puisqu'un commit sans check n'a pas été vérifié du tout. L'UI l'affiche comme "No checks have run against this commit", sans coche.

Si vous demandez à Claude Code de répondre à « est-ce que c'est vert ? », donnez-lui ces quatre règles dans CLAUDE.md. L'outil renvoie des statuts, pas un verdict.

Pourquoi un rerun embarque-t-il une version et un commit ?

Parce que la personne qui approuve regarde un commit précis, et que le change set peut bouger avant qu'elle clique.

Dans GitNest, un change set est composé à partir de résultats sauvegardés en un seul commit. S'il est recomposé, après un nouveau résultat sauvegardé ou un déplacement de la base, il reçoit une nouvelle version et un nouveau commit composé, et ses checks repartent de zéro. Une proposition de rerun attend un humain. Si elle disait seulement "rerun e2e on cs_4e81a0", l'approbation relancerait e2e sur ce qu'est devenu le change set entre-temps, peut-être un commit que le propriétaire n'a jamais vu.

checks_sim.py modélise cette attente. Chaque essai est une proposition. L'agent lit la version 3, commit A, avec e2e en échec. Le temps de revue du propriétaire suit une loi log-normale de médiane 8 minutes. C'est une hypothèse, pas une mesure. Pendant l'attente, le change set est recomposé selon un processus de Poisson, en moyenne toutes les 45 minutes, et après une recomposition les checks mettent 4 minutes à tourner. Dans 10 % des essais, une deuxième session Claude Code a lu le même échec et déposé sa propre proposition avec sa propre clé, et le propriétaire approuve les deux. Trois garde-fous au moment de l'approbation, 10 000 essais chacun :

== approval-time guards (n=10000, review median 8 min, recompose mean every 45 min, 10% second session) ==
guard          ran on reviewed commit ran on unreviewed commit            duplicate run          refused (stale)   refused (not finished)
none                             7789                     1411                     1764                        0                        0
version+oid                      7789                        0                      740                     2435                        0
full                             7789                        0                        0                     2435                      740
Enter fullscreen mode Exit fullscreen mode

Sans garde-fou, 1 411 approbations ont relancé un check sur un commit que le propriétaire n'avait pas relu, et 1 764 ont mis en file un check déjà en file ou en cours. Comparer version et commit à l'approbation fait tomber les runs non relus à 0 et refuse à la place 2 435 cartes périmées. Les 740 doublons restants viennent des deuxièmes sessions : même version, même commit, mais la première approbation avait déjà remis le check en file. Le troisième garde-fou, « ce check est-il toujours terminé ? », les attrape. C'est l'ensemble complet que liste la description de l'outil : "Approval rechecks repository access, version, commit and check state."

La fréquence de recomposition est l'entrée la moins sûre, alors voici la ligne sans garde-fou à trois fréquences :

== sensitivity: unreviewed-commit runs with guard 'none' ==
recompose every  15 min  unreviewed  2449  duplicates 3128
recompose every  45 min  unreviewed  1411  duplicates 1764
recompose every 180 min  unreviewed   452  duplicates 1208
Enter fullscreen mode Exit fullscreen mode

Même un change set qui bouge toutes les trois heures donne 452 runs sur le mauvais commit pour 10 000. Le garde-fou ne coûte rien dans le cas nominal : le nombre de runs sur le commit relu est de 7 789 dans les trois lignes.

Pourquoi à la fois la version et le commit ?

Dans ce modèle, ils bougent toujours ensemble, donc l'un ou l'autre seul donnerait le même score. Ils ne répondent pourtant pas à la même question. La version est le compteur propre du change set, elle bouge quand son contenu change. Le commit est ce sur quoi les checks ont réellement tourné, et c'est ce que la carte affiche comme "Reviewed commit: …". Exiger les deux du client permet au serveur de refuser sur l'un ou l'autre, et le propriétaire lit un hash de commit sur la carte, pas un compteur interne. Le schéma impose la version comme entier ≥ 1. Le commit est une chaîne libre, sa vérification se fait donc côté serveur.

Qu'est-ce que le schéma intercepte avant l'appel ?

Le script valide les arguments contre l'inputSchema en production avec jsonschema :

== propose_gitnest_check_rerun arguments ==
ok                 PASS
no oid             REJECT  'expected_composition_oid' is a required property
version 0          REJECT  0 is less than the minimum of 1
version as string  REJECT  '3' is not of type 'integer'
two checks         REJECT  ['e2e', 'lint'] is not of type 'string'
check 201 chars    REJECT  'ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
key 7 chars        REJECT  'abcdefg' is too short
extra 'force'      REJECT  Additional properties are not allowed ('force' was unexpected)
required: repository_id, change_set_id, check_name, expected_change_set_version, expected_composition_oid, idempotency_key
Enter fullscreen mode Exit fullscreen mode

Deux de ces rejets relèvent du design, pas de l'hygiène. check_name est une chaîne, pas une liste, donc « tout relancer » donne une carte par check et le propriétaire décide pour chacune. Et additionalProperties: false fait qu'un modèle qui invente "force": true reçoit une erreur au lieu d'un flag ignoré en silence.

La clé d'idempotence doit dériver de l'objet de la proposition, pas de la tentative. Le script utilise "rerun-" + sha256(change_set_id, version, oid, check_name)[:32] :

key retry same: True   key after recompose same: False   len 38
Enter fullscreen mode Exit fullscreen mode

Un retry après une réponse perdue envoie la même clé et ne crée pas de deuxième carte. Une recomposition change la clé, donc une proposition pour le nouveau commit est une nouvelle carte, et c'est ce qu'on veut.

Combien pèsent ces outils, et à quelle distance est le serveur ?

== tools/list ==
59 tools, 48140 B via json.dumps, 12722 tok (cl100k)
list_gitnest_checks              612 B  158 tok  readOnly=True  destructive=False scope=osfoundry:read
list_gitnest_saved_results       707 B  182 tok  readOnly=True  destructive=False scope=osfoundry:read
read_gitnest_work                685 B  189 tok  readOnly=True  destructive=False scope=osfoundry:read
propose_gitnest_check_rerun     1011 B  268 tok  readOnly=False destructive=True  scope=osfoundry:write
all 13 gitnest tools 11227 B 3009 tok
Enter fullscreen mode Exit fullscreen mode

Les 13 outils GitNest représentent environ un quart des tokens du catalogue (comptés avec tiktoken cl100k_base, donc à prendre comme une approximation). Dix appels non authentifiés de chaque type depuis ce Mac au Japon vers l'endpoint en production :

$ python3 probe.py
initialize                       HTTP 200     308 B  median    566 ms  min   326  max   807
  capabilities: {"tools": {"listChanged": false}}
tools/list                       HTTP 200   45368 B  median    722 ms  min   420  max  1406
tools/call list_gitnest_checks   HTTP 401     337 B  median    552 ms  min   409  max   903
Enter fullscreen mode Exit fullscreen mode

Le 401 porte error_description="Connect osFoundry to continue" et une URL resource_metadata, et c'est ce qui envoie Claude Code dans son flux OAuth. initialize n'annonce aucun push, donc rien ne prévient le client que le retry a été approuvé ou que le check est terminé. Attendre un check, c'est le travail d'un runner sur votre machine plus le clic d'un humain : des minutes, pas des millisecondes. Le schéma le moins coûteux consiste à terminer le tour après la proposition et à appeler list_gitnest_checks une fois quand l'utilisateur dit qu'il a approuvé.

Qui exécute vraiment le check ?

Pas osFoundry. Le panneau des runners de GitNest indique que les checks "run on your machine, not ours". Un runner enregistré fait du polling avec son propre token (OSF_RUNNER_TOKEN=… osfoundry-runner --api https://api.osfoundry.io), et sans runner les checks "stay queued". Approuver un retry ne fait que remettre le check en file. Claude Code ne détient jamais le token du runner, et les outils de lecture omettent les bindings locaux et les identifiants. L'agent voit que e2e a échoué. Il ne peut rien faire exécuter sans vous et votre runner.

Si vous câblez ça vous-même

  • Mettez la règle de verdict dans les instructions du client, pas seulement dans votre UI. « Aucun check » et « statut inconnu » sont précisément les cas où les agents annoncent du vert.
  • Faites en sorte qu'un rerun nomme le commit sur lequel il a été relu, et comparez au moment de l'approbation, pas à celui de la proposition.
  • Revérifiez aussi l'état du check lui-même à l'approbation. Version et commit ne détectent pas deux propositions pour le même échec.
  • Un seul check par proposition. Une liste transforme le retry d'un test instable en rerun général que personne n'a choisi.
  • Dérivez la clé d'idempotence de (change set, version, commit, check), pour que les retries fusionnent et pas les recompositions.
  • Gardez l'exécution hors de la plateforme et hors de l'agent : le runner détient le token, l'humain détient le bouton.

La page de revue du change set avec le panneau des checks se trouve sur la page GitNest. Pour le protocole lui-même, voir l'entrée du glossaire sur MCP.

Top comments (0)