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"]}]
}
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 :
- Aucun run, c'est
none, jamaispassed. - Le moindre
failure,timed_outoucancelleddonnefailed. - Tout ce qui n'est pas encore
successlaisse l'état àrunning, y compris un statut que le client ne reconnaît pas. - 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
« 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
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
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
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
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
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
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)