Questo è il tool che Claude Code chiama quando vuole rieseguire un check GitNest fallito, così come lo elenca l'endpoint MCP in produzione di osFoundry in tools/list (per il catalogo non serve un token):

{ "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"]}] } 

La sua controparte in lettura accetta due argomenti: list_gitnest_checks {repository_id, change_set_id}, "bounded check status for the current composed commit of a change set". Altre due letture alimentano la decisione: list_gitnest_saved_results (limit 1–50) e read_gitnest_work (kind è tasks, changes o runs). Tutte e tre sono osfoundry:read con readOnlyHint: true.

Sei campi obbligatori per rilanciare un solo check sono tanti. A un pulsante di CI basta l'ID di un job. I due in più, una versione e un commit, sono l'argomento di questo post. Su questo Mac non c'è nessun binario claude installato, quindi è checks_sim.py a fare da client. Il codice e il suo output completo sono in code/.

Cosa vuol dire "verde" quando un agente legge i check?

Prima di proporre qualsiasi cosa, l'agente deve leggere correttamente la lista dei check. La UI di GitNest in osFoundry riassume i check run di un commit con una piccola funzione pura (summarise in check-runs.ts). L'ordine dei suoi controlli è la regola:

  1. Nessun run è none, mai passed.
  2. Qualsiasi failure, timed_out o cancelled è failed.
  3. Tutto ciò che non è ancora success lo lascia in running, compreso uno stato che il client non riconosce.
  4. Solo allora è passed.

Lo script porta questa regola e la confronta con due scorciatoie che un modello scrive da solo:

== 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 

"Non è fallito niente, quindi è verde" sbaglia in 5 casi su 7. Dichiara verde un commit mai verificato, un check in coda e un timeout. Il controllo sul rapporto (passed == total) va più vicino, ma con zero check 0 == 0 e dice verde anche lui. È proprio il caso che i commenti nel codice di GitNest indicano come il più fuorviante, perché un commit senza check non è stato verificato affatto. La UI lo mostra come "No checks have run against this commit", senza spunta.

Se chiedi a Claude Code di rispondere a "è verde?", dagli queste quattro regole in CLAUDE.md. Il tool restituisce stati, non un verdetto.

Perché un rerun porta con sé una versione e un commit?

Perché chi approva sta guardando un commit preciso, e il change set può spostarsi prima che faccia clic.

In GitNest un change set viene composto in un unico commit a partire da risultati salvati. Se viene ricomposto, dopo un nuovo risultato salvato o uno spostamento della base, riceve una nuova versione e un nuovo commit composto, e i suoi check ripartono da zero. Una proposta di rerun aspetta un umano. Se dicesse solo "rerun e2e on cs_4e81a0", l'approvazione rilancerebbe e2e su qualunque cosa sia diventato il change set nel frattempo, magari un commit che il proprietario non ha mai visto.

checks_sim.py modella questa attesa. Ogni prova è una proposta. L'agente legge la versione 3, commit A, con e2e fallito. Il tempo di revisione del proprietario è log-normale con mediana di 8 minuti. È un'ipotesi, non una misura. Durante l'attesa il change set viene ricomposto come un processo di Poisson, in media ogni 45 minuti, e dopo una ricomposizione i check impiegano 4 minuti a girare. Nel 10% delle prove una seconda sessione di Claude Code ha letto lo stesso fallimento e ha inviato la propria proposta con la propria chiave, e il proprietario le approva entrambe. Tre controlli al momento dell'approvazione, 10.000 prove ciascuno:

== 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 

Senza controlli, 1.411 approvazioni hanno rilanciato un check su un commit che il proprietario non aveva revisionato, e 1.764 hanno messo in coda un check già in coda o in esecuzione. Confrontare versione e commit all'approvazione porta i run non revisionati a 0 e in cambio rifiuta 2.435 card scadute. I 740 duplicati rimasti sono le seconde sessioni: stessa versione, stesso commit, ma la prima approvazione aveva già rimesso in coda il check. Il terzo controllo, "questo check è ancora terminato?", li intercetta. È l'insieme completo che elenca la descrizione del tool: "Approval rechecks repository access, version, commit and check state."

La frequenza di ricomposizione è il dato di input più incerto, quindi ecco la riga senza controlli con tre frequenze:

== 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 

Anche un change set che si sposta ogni tre ore produce 452 run sul commit sbagliato ogni 10.000. Nel caso normale il controllo non costa nulla: il conteggio sul commit revisionato è 7.789 in tutte e tre le righe.

Perché sia la versione sia il commit?

In questo modello si muovono sempre insieme, quindi uno solo dei due darebbe lo stesso risultato. Però rispondono a domande diverse. La versione è il contatore interno del change set e cambia quando ne cambia il contenuto. Il commit è ciò su cui i check sono stati effettivamente eseguiti, ed è quello che la card mostra come "Reviewed commit: …". Chiedere al client di inviarli entrambi permette al server di rifiutare per l'uno o per l'altro, e il proprietario legge sulla card un hash di commit, non un contatore interno. Lo schema impone la versione come intero ≥ 1. Il commit è una stringa libera, quindi il suo controllo avviene sul server.

Cosa blocca lo schema prima della chiamata?

Lo script valida gli argomenti contro l'inputSchema in produzione con 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 

Due dei rifiuti sono una scelta di design, non di igiene. check_name è una stringa, non una lista, quindi "rilancia tutto" significa una card per check e il proprietario decide su ciascuna. E additionalProperties: false fa sì che un modello che si inventa "force": true riceva un errore invece di un flag ignorato in silenzio.

La chiave di idempotenza dovrebbe derivare dall'oggetto della proposta, non dal tentativo. Lo script usa "rerun-" + sha256(change_set_id, version, oid, check_name)[:32]:

key retry same: True key after recompose same: False len 38 

Un retry dopo una risposta persa invia la stessa chiave e non crea una seconda card. Una ricomposizione cambia la chiave, quindi una proposta per il nuovo commit è una nuova card, ed è giusto così.

Quanto pesano questi tool e quanto è lontano il server?

== 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 

I 13 tool di GitNest valgono circa un quarto dei token del catalogo (contati con tiktoken cl100k_base, quindi da prendere come approssimazione). Dieci chiamate non autenticate per tipo da questo Mac in Giappone verso l'endpoint in produzione:

$ 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 

Il 401 contiene error_description="Connect osFoundry to continue" e un URL resource_metadata, ed è questo che manda Claude Code nel suo flusso OAuth. initialize non annuncia alcun push, quindi niente avvisa il client che il retry è stato approvato o che il check è finito. Aspettare un check significa il lavoro di un runner sulla tua macchina più il clic di una persona: minuti, non millisecondi. Lo schema più economico è chiudere il turno dopo la proposta e chiamare list_gitnest_checks una volta quando l'utente dice di aver approvato.

Chi esegue davvero il check?

Non osFoundry. Il pannello dei runner di GitNest dice che i check "run on your machine, not ours". Un runner registrato fa polling con il proprio token (OSF_RUNNER_TOKEN=… osfoundry-runner --api https://api.osfoundry.io), e senza runner i check "stay queued". Approvare un retry si limita a rimettere il check in coda. Claude Code non ha mai il token del runner, e i tool di lettura escludono binding locali e credenziali. L'agente può vedere che e2e è fallito. Non può far eseguire nulla senza di te e il tuo runner.

Se lo colleghi tu

  • Metti la regola del verdetto nelle istruzioni del client, non solo nella tua UI. "Nessun check" e "stato sconosciuto" sono proprio i punti in cui gli agenti dicono verde.
  • Fai in modo che un rerun indichi il commit su cui è stato revisionato, e confronta al momento dell'approvazione, non della proposta.
  • All'approvazione ricontrolla anche lo stato del check stesso. Versione e commit non intercettano due proposte per lo stesso fallimento.
  • Un solo check per proposta. Una lista trasforma il retry di un test instabile in un rerun generale che nessuno ha scelto.
  • Deriva la chiave di idempotenza da (change set, versione, commit, check), così i retry si fondono e le ricomposizioni no.
  • Tieni l'esecuzione fuori dalla piattaforma e fuori dall'agente: il runner ha il token, la persona ha il pulsante.

La pagina di revisione del change set con il pannello dei check è nella pagina di GitNest. Per il protocollo in sé, vedi la voce del glossario su MCP.