DEV Community

Cover image for ¿Por qué osFoundry obliga a Claude Code a indicar el commit cuando pide volver a ejecutar un check de GitNest?
BBT for BBT Español

Posted on

¿Por qué osFoundry obliga a Claude Code a indicar el commit cuando pide volver a ejecutar un check de GitNest?

Esta es la herramienta que llama Claude Code cuando quiere que un check fallido de GitNest se vuelva a ejecutar, tal como la lista el endpoint MCP en producción de osFoundry en tools/list (para el catálogo no hace falta 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

Su pareja de lectura recibe dos argumentos: list_gitnest_checks {repository_id, change_set_id}, "bounded check status for the current composed commit of a change set". Otras dos lecturas alimentan la decisión: list_gitnest_saved_results (limit 1–50) y read_gitnest_work (kind es tasks, changes o runs). Las tres son osfoundry:read con readOnlyHint: true.

Seis campos obligatorios para relanzar un solo check son muchos. Un botón de CI necesita un ID de job. Los dos extra, una versión y un commit, son el tema de este post. En este Mac no hay ningún binario claude instalado, así que checks_sim.py hace de cliente. El código y su salida completa están en code/.

¿Qué significa "verde" cuando un agente lee los checks?

Antes de proponer nada, el agente tiene que leer bien la lista de checks. La UI de GitNest en osFoundry resume los check runs de un commit con una pequeña función pura (summarise en check-runs.ts). El orden de sus comprobaciones es la regla:

  1. Sin ningún run es none, nunca passed.
  2. Cualquier failure, timed_out o cancelled es failed.
  3. Todo lo que todavía no sea success lo deja en running, incluido un estado que el cliente no reconoce.
  4. Solo entonces es passed.

El script porta esa regla y la compara con dos atajos que un modelo escribe por su cuenta:

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

"No ha fallado nada, así que está en verde" se equivoca en 5 de 7 casos. Da por verde un commit sin examinar, un check en cola y un timeout. La comprobación por proporción (passed == total) se acerca más, pero con cero checks 0 == 0 y también dice verde. Es justo el caso que los comentarios del código de GitNest señalan como el más engañoso, porque un commit sin checks no se ha verificado en absoluto. La UI lo muestra como "No checks have run against this commit", sin tick.

Si le pides a Claude Code que responda "¿está en verde?", dale estas cuatro reglas en CLAUDE.md. La herramienta devuelve estados, no un veredicto.

¿Por qué un rerun lleva una versión y un commit?

Porque la persona que lo aprueba está mirando un commit concreto, y el change set puede moverse antes de que haga clic.

En GitNest, un change set se compone a partir de resultados guardados en un único commit. Si se recompone, por un nuevo resultado guardado o porque se movió la base, recibe una versión nueva y un commit compuesto nuevo, y sus checks empiezan de cero. Una propuesta de rerun espera a un humano. Si solo dijera "rerun e2e on cs_4e81a0", la aprobación relanzaría e2e sobre lo que sea el change set en ese momento, que puede ser un commit que el propietario nunca vio.

checks_sim.py modela esa espera. Cada prueba es una propuesta. El agente lee la versión 3, commit A, con e2e fallido. El tiempo de revisión del propietario sigue una log-normal con mediana de 8 minutos. Es una suposición, no una medición. Durante la espera, el change set se recompone como un proceso de Poisson, de media cada 45 minutos, y tras una recomposición los checks tardan 4 minutos en ejecutarse. En el 10 % de las pruebas, una segunda sesión de Claude Code leyó el mismo fallo y presentó su propia propuesta con su propia clave, y el propietario aprueba ambas. Tres guardas en el momento de la aprobación, 10.000 pruebas cada una:

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

Sin guarda, 1.411 aprobaciones relanzaron un check sobre un commit que el propietario no había revisado, y 1.764 encolaron un check que ya estaba en cola o ejecutándose. Comparar versión y commit al aprobar baja las ejecuciones sin revisar a 0 y, a cambio, rechaza 2.435 tarjetas obsoletas. Los 740 duplicados que quedan son las segundas sesiones: misma versión, mismo commit, pero la primera aprobación ya había vuelto a encolar el check. La tercera guarda, "¿sigue terminado este check?", los atrapa. Ese es el conjunto completo que enumera la descripción de la herramienta: "Approval rechecks repository access, version, commit and check state."

La frecuencia de recomposición es el dato de entrada menos fiable, así que aquí va la fila sin guarda con tres frecuencias:

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

Incluso un change set que se mueve cada tres horas da 452 ejecuciones sobre el commit equivocado por cada 10.000. La guarda no cuesta nada en el camino feliz: el recuento sobre el commit revisado es 7.789 en las tres filas.

¿Por qué la versión y el commit a la vez?

En este modelo siempre se mueven juntos, así que cualquiera de los dos por separado puntuaría igual. Pero responden a preguntas distintas. La versión es el contador propio del change set y cambia cuando cambia su contenido. El commit es sobre lo que realmente se ejecutaron los checks, y es lo que la tarjeta muestra como "Reviewed commit: …". Pedir al cliente que envíe ambos permite al servidor rechazar por cualquiera de los dos, y el propietario lee un hash de commit en la tarjeta, no un contador interno. El schema impone la versión como entero ≥ 1. El commit es un string libre, así que su comprobación se hace en el servidor.

¿Qué atrapa el schema antes de la llamada?

El script valida los argumentos contra el inputSchema en producción 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

Dos de los rechazos son diseño, no higiene. check_name es un string, no una lista, así que "relanzarlo todo" supone una tarjeta por check y el propietario decide cada una. Y additionalProperties: false hace que un modelo que se inventa "force": true reciba un error en lugar de un flag ignorado en silencio.

La clave de idempotencia debería salir de aquello sobre lo que trata la propuesta, no del intento. El 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 reintento tras perder la respuesta envía la misma clave y no crea una segunda tarjeta. Una recomposición cambia la clave, así que una propuesta para el commit nuevo es una tarjeta nueva, que es lo correcto.

¿Cuánto pesan estas herramientas y a qué distancia está el servidor?

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

Las 13 herramientas de GitNest son más o menos una cuarta parte de los tokens del catálogo (contados con tiktoken cl100k_base, así que tómalos como aproximados). Diez llamadas sin autenticar de cada tipo desde este Mac en Japón al endpoint en producción:

$ 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

El 401 trae error_description="Connect osFoundry to continue" y una URL resource_metadata, que es lo que lleva a Claude Code a su flujo OAuth. initialize no anuncia push, así que nada avisa al cliente de que el reintento se aprobó o de que el check terminó. Esperar a un check es trabajo de un runner en tu máquina más el clic de una persona: minutos, no milisegundos. El patrón barato es terminar el turno después de proponer y llamar a list_gitnest_checks una vez cuando el usuario diga que lo ha aprobado.

¿Quién ejecuta realmente el check?

osFoundry no. El panel de runners de GitNest dice que los checks "run on your machine, not ours". Un runner registrado hace polling con su propio token (OSF_RUNNER_TOKEN=… osfoundry-runner --api https://api.osfoundry.io), y sin él los checks "stay queued". Aprobar un reintento solo vuelve a encolar el check. Claude Code nunca tiene el token del runner, y las herramientas de lectura dejan fuera los bindings locales y las credenciales. El agente puede ver que e2e falló. No puede hacer que se ejecute nada sin ti y tu runner.

Si lo montas tú

  • Pon la regla del veredicto en las instrucciones del cliente, no solo en tu UI. "Sin checks" y "estado desconocido" son justo donde los agentes dicen verde.
  • Haz que un rerun indique el commit contra el que se revisó, y compara en el momento de aprobar, no en el de proponer.
  • Vuelve a comprobar también el estado del propio check al aprobar. Versión y commit no detectan dos propuestas para el mismo fallo.
  • Acepta un solo check por propuesta. Una lista convierte el reintento de un test inestable en un rerun general que nadie eligió.
  • Deriva la clave de idempotencia de (change set, versión, commit, check), para que los reintentos se fusionen y las recomposiciones no.
  • Deja la ejecución fuera de la plataforma y fuera del agente: el runner tiene el token, la persona tiene el botón.

La página de revisión del change set con el panel de checks está en la página de GitNest. Para el protocolo en sí, mira la entrada del glosario sobre MCP.

Top comments (0)