¿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"]}] } 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: Sin ningún run es none, nunca passed. Cualquier failure, timed_out o cancelled es failed. Todo lo que todavía no sea success lo deja en running, incluido un estado que el cliente no reconoce. 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 "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 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 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 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 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 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 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.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to