DEV Community

Cover image for Perché osFoundry obbliga Claude Code a indicare il commit quando chiede di rilanciare un check GitNest?
BBT for BBT Italiano

Posted on

Perché osFoundry obbliga Claude Code a indicare il commit quando chiede di rilanciare un check GitNest?

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"]}]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)