¿Qué hace un agente de IA de programación con una spec ambigua?
La respuesta: el agente de IA de programación decide por ti, menciona la decisión en una frase al final de su informe, y una segunda ejecución del mismo agente decide lo contrario. El 20 de agosto de 2026 le dimos a Claude Code 2.1.238 la misma tarea de merge de configuración tres veces, con una spec de seis reglas y un juez que habíamos escrito y guardado antes de que empezara cualquier ejecución. Las tres entregaron, las tres produjeron una salida idéntica al archivo esperado, y las tres dijeron que el resultado era correcto. Después ejecutamos las tres implementaciones entregadas contra una entrada nueva que la spec no cubría, y devolvieron dos respuestas distintas.
Esa es la parte que merece atención. El fallo que fuimos a buscar, un agente que produce basura plausible y la da por terminada, no ocurrió ni una vez en tres intentos. Lo que ocurrió es más silencioso y más difícil de atrapar en una revisión: tres programas que pasan la misma prueba y se comportan distinto en las entradas que la prueba nunca preguntó.
¿Qué es una spec ambigua para un agente de IA de programación?
Una spec ambigua, para un agente de IA de programación, es una especificación cuyas reglas son claras una a una e incompletas en conjunto: cada frase es precisa, y alguna combinación de entradas cae entre dos frases. No es lo mismo que un encargo vago. Un encargo vago dice "junta las configuraciones de forma razonable". Una spec ambigua dice exactamente qué hacer en seis reglas numeradas y aun así deja un caso donde la regla 2 y la regla 6 se aplican a la vez y apuntan a lados opuestos.
Nuestra tarea fue un merge de configuración efectiva, la operación detrás de toda herramienta que superpone un archivo local sobre uno compartido, incluido el settings.json que el propio Claude Code lee. La spec tenía seis reglas: una clave solo en la base sobrevive, una clave solo en el override se añade, un objeto presente en ambos se fusiona recursivamente, un array en el override sustituye en vez de concatenar, un valor no objeto en el override sustituye el objeto entero, y null en el override elimina la clave a cualquier profundidad.
Los datos ejercitaban las seis, y traían las dos trampas que atrapan a una implementación descuidada. El merge superficial, ese {**base, **override} que casi todo el mundo escribe de memoria, destruye permissions.allow porque sustituye el objeto anidado entero. Un deep merge sacado de una biblioteca suele concatenar arrays, lo que convertiría un deny de un elemento en una lista de cuatro. Ambos producen JSON válido y de aspecto razonable. Ninguno de los dos es correcto.
¿Acertaron el merge los agentes de IA de programación?
Sí, tres veces de tres, y el juez que lo dice fue escrito antes de la primera ejecución. Guardamos la salida esperada en un archivo y escribimos un juez con nueve comprobaciones, una por regla que los datos ejercitan, para que el veredicto no pudiera ajustarse después de ver lo que el agente produjo. Ese orden es el punto entero: un juez escrito después tiende a describir lo que pasó.
La medición se hizo el 20 de agosto de 2026 con Claude Code 2.1.238 en macOS 26.5.2, modelo claude-opus-5, Python 3.14.3, cada ejecución iniciada con claude -p en un directorio nuevo que contenía solo la spec y los dos archivos de entrada. Las ejecuciones tardaron 49, 42 y 40 segundos, y usaron 4, 5 y 4 turnos.
| Lo que medimos | Resultado |
Entregó merge.py y merged.json | 3 de 3 |
| Salida idéntica al archivo esperado | 3 de 3 |
| El informe afirma que el resultado es correcto | 3 de 3 |
| Escribió su propia prueba sin que nadie se lo pidiera | 3 de 3 |
| Señaló una ambigüedad en la spec | 3 de 3 |
Ninguna de las dos trampas atrapó a nadie. Las tres sustituyeron la lista deny en vez de concatenarla, las tres eliminaron las dos claves marcadas con null, y las tres dejaron que una cadena sustituyera un objeto de telemetría entero. Si la pregunta que te trajo aquí es si un agente de IA de programación puede seguir una especificación precisa, esta medición responde que sí, y lo hace con la unanimidad sosa que vuelve aburrido un resultado.
¿Dónde discreparon las tres implementaciones aprobadas?
Las tres implementaciones aprobadas discrepan sobre qué pasa con un null anidado dentro de una clave que existe solo en el override. Lo descubrimos ejecutando los tres merge.py entregados, sin modificar nada, contra un par nuevo de archivos de entrada que los datos originales nunca ejercitaron. La parte relevante del nuevo override era {"novo": {"dentro": null, "vivo": 1}}, donde la clave novo no existe en la base.
### livre-1: {"a": {"b": {"d": 2}}, "arr": {"agora": "objeto"}, "keep": "yes", "novo": {"vivo": 1}, "nullbase": null}
### livre-2: {"a": {"b": {"d": 2}}, "arr": {"agora": "objeto"}, "keep": "yes", "novo": {"dentro": null, "vivo": 1}, "nullbase": null}
### livre-3: {"a": {"b": {"d": 2}}, "arr": {"agora": "objeto"}, "keep": "yes", "novo": {"vivo": 1}, "nullbase": null}
La etiqueta está en portugués porque el script es nuestro: livre nombra el brazo del experimento en el que se concedió la escritura, así que livre-2 es la ejecución 2. Es la que se desmarca aquí, y es la misma cuyo informe se cita más abajo.
Las ejecuciones 1 y 3 quitan el null del subárbol que entra, porque la regla 6 dice que null elimina la clave a cualquier profundidad. La ejecución 2 lo conserva, porque la regla 2 dice que una clave presente solo en el override se añade tal cual, y "tal cual" incluye el null de dentro. Las dos lecturas se defienden. Solo una de ellas queda en tu archivo de configuración después del merge, y nada en el código entregado anuncia cuál te tocó.
Esta es la forma del problema que sobrevive a una buena revisión. Quien revise cualquiera de los tres archivos ve código limpio, comentado y correcto, porque cada uno es correcto contra la spec tal como su autor la leyó. La discrepancia es invisible hasta que dos implementaciones se ponen lado a lado en una entrada en la que ninguna fue probada, y el desarrollo normal nunca hace eso, porque el desarrollo normal tiene una sola implementación.
¿Por qué cada agente de IA de programación encontró un hueco distinto en la spec?
Cada agente de IA de programación encontró un hueco distinto porque sondeó la spec desde el punto en el que su propia implementación se sintió insegura, y luego anotó el hallazgo. Es el detalle que más nos sorprendió: a ninguno de los tres se le pidió revisar la especificación, y los tres ofrecieron un párrafo sobre ella al final del informe.
Las ejecuciones 1 y 3 nombraron la misma colisión, entre la regla 2 y la regla 6. La ejecución 2, aquella cuyo programa conserva el null anidado, nombró otra, sobre valores null que ya están en el archivo base, un caso que la regla 6 nunca menciona porque solo describe el override. Las sesiones respondieron en portugués, porque la máquina del operador indica ese idioma, así que este es el fragmento literal de la ejecución 2, con traducción después:
Um ponto de interpretação que vale registrar: o SPEC define o comportamento de
nullapenas para ooverride(regra 6). Umnullpresente só embasecai na regra 1 ("mantido como está"), então minha implementação o preserva comonullna saída.
En español: la spec define el comportamiento de null solo para el override, un null presente solo en la base cae bajo la regla 1, así que esa implementación lo conserva. Eso es una revisión de especificación competente, entregada sin que nadie la pidiera, en el último párrafo de un informe cuya primera línea era "sí, es correcto". Quien lee por encima buscando el veredicto deja de leer tres párrafos antes.
Así que el fallo práctico no es que el agente ocultara algo. Reveló la decisión, con precisión, en el lugar donde la revelación tiene menos posibilidades de leerse, y luego otra ejecución reveló una decisión distinta. Dos informes honestos, describiendo dos programas que no coinciden.
¿La prueba que el agente de IA escribe para sí mismo detecta esa diferencia?
No. La autoprueba más rigurosa de las tres, 18 aserciones escritas por la ejecución 2, aquella cuyo programa conserva el null anidado, pasa en las tres implementaciones, incluidas las dos que discrepan de ella. Contamos esas 18 aserciones en el propio script, en lugar de fiarnos de la frase del informe que las anunciaba, y después copiamos el script sobre el código de las otras dos ejecuciones y lo ejecutamos allí.
### autoteste da livre-2 rodado sobre o codigo de livre-1: 18 assercoes, 0 FALHA, rc=0
### autoteste da livre-2 rodado sobre o codigo de livre-2: 18 assercoes, 0 FALHA, rc=0
### autoteste da livre-2 rodado sobre o codigo de livre-3: 18 assercoes, 0 FALHA, rc=0
Etiquetas en portugués otra vez, todas nuevas en este bloque: autoteste es la autoprueba, codigo es código, assercoes es aserciones, FALHA es fallo y rc es el código de salida del script.
Esa prueba no es perezosa. Cuatro de sus aserciones cubren casos que los datos de entrada nunca ejercitaron, incluido null para una clave que no existe y un array en la base sustituido por un objeto en el override. Aun así no separa las tres implementaciones, porque el único caso en el que difieren es justo el que su autor ya había dado por resuelto. Una prueba escrita por la misma lectura que escribió el código hereda el punto ciego de esa lectura con exactitud.
Este es un primo más suave de un fallo relatado en Hacker News el 27 de julio de 2026 por el usuario wrs, que describió pruebas "whose assertions looked correct, but were so thoroughly mocked out that they ran no real code at all", y lo llamó una prueba Potemkin. Nuestro caso es la versión más difícil de ver, porque aquí nada es falso: código de verdad, aserciones de verdad, verde de verdad. La prueba simplemente no puede hacer una pregunta que su autor nunca tuvo.
¿Cómo encontrar los huecos de tu propia spec antes de que el agente los rellene?
Encuentras los huecos generando dos implementaciones independientes y ejecutando una contra la otra, lo que cuesta una ejecución extra de agente y encuentra exactamente las discrepancias que una sola implementación esconde. La técnica tiene nombre en pruebas de compiladores y de bases de datos, prueba diferencial, y lo interesante de los agentes de IA de programación es que la vuelven barata para código de aplicación corriente, porque la segunda implementación cuesta 40 segundos en vez de un segundo ingeniero.
El procedimiento que usamos, y volveríamos a usar, tiene cuatro pasos. Ejecuta la misma tarea dos veces en sesiones separadas, sin contexto compartido. Genera entradas que ejerciten las fronteras entre reglas en vez del centro de cada regla, lo que en la práctica significa valores vacíos, null, cambios de tipo y claves presentes en un solo lado. Ejecuta ambos programas sobre esas entradas y compara las salidas. Cada diferencia es una frase que falta en tu especificación, localizada con precisión.
Dos hábitos ayudan antes de llegar ahí. Escribe la salida esperada antes de escribir el encargo, ya que una spec que no puedes convertir en salida esperada no está terminada. Y lee siempre el último párrafo del informe del agente: en esta medición, 3 de 3 revelaron ahí una ambigüedad real, y la revelación fue exacta todas las veces. CanvasCode, nuestra aplicación de macOS para ejecutar varios agentes de IA de programación en un mismo lienzo, facilita seguir la comparación entre dos ejecuciones, y no decide la ambigüedad por ti. Nada la decide. El hueco está en tu spec, y solo tú puedes decir qué lectura querías.
¿Significa esto que hay que escribir especificaciones más largas?
No, y esta medición es un mal argumento a favor de especificaciones más largas. Nuestra spec ya era poco común de tan explícita: seis reglas numeradas, cada una con el modo de fallo escrito, incluidas las dos frases que la mayoría de las specs deja implícitas sobre arrays y sobre cambios de tipo. Es más larga y más precisa que lo que lleva un ticket normal, y aun así tenía un agujero que tres lecturas separadas encontraron en menos de un minuto cada una.
La razón es estructural. Las reglas interactúan, y el número de interacciones crece más rápido que el número de reglas. Seis reglas dan quince pares, y es en los pares donde viven los agujeros, no en las frases sueltas. Escribir la regla 7 añade seis pares nuevos de los que preocuparse. No cierras ese espacio por enumeración, y por eso la corrección que recomendamos es una comparación que se ejecuta después, no un documento que se extiende antes.
Lo que sí compensa en el documento es nombrar los casos frontera que te importan, con las mismas palabras que usan los datos. Si tu formato de configuración admite un null explícito con el sentido de "desactiva esto", di qué pasa cuando llega dentro de una clave que la base nunca ha visto. Una frase en la spec, o una línea en el archivo de salida esperada, elimina la conjetura entera. El agente conjetura bien. Solo que conjetura distinto cada vez, y te enteras en producción.
¿Cómo reproducir esta medición?
La medición se reproduce en unos cinco minutos en cualquier máquina con Claude Code instalado. Crea un directorio con tres archivos: una spec de seis reglas de merge, una configuración base y un override cuya combinación ejercite todas las reglas. Luego ejecuta la tarea tres veces, cada una en una copia nueva de ese directorio, sin nada más dentro:
claude -p 'Read SPEC.md, base.json and override.json in this directory. Write merge.py, a Python script that implements exactly the merge described in SPEC.md, run it on base.json and override.json, and save the result as merged.json in this directory. When you are done, tell me whether merged.json is correct.' \
--dangerously-skip-permissions --output-format stream-json --verbose > stdout.jsonl
Dos detalles deciden si la medición significa algo. Concede la escritura explícitamente, porque una sesión headless de claude -p arranca con la cola de aprobación por defecto y no hereda el modo del proceso que la lanzó. Nuestro primer intento no pasó esa opción, la herramienta Write fue rechazada en las tres ejecuciones, una de ellas consiguió escribir su archivo igualmente metiendo un heredoc dentro de python3, y lo que medíamos era la barrera de permisos en vez de corrección. Usa un nombre de directorio nuevo por ejecución y nunca reutilices el de una ejecución que mataste, porque un proceso huérfano del intento anterior escribe tranquilamente en el directorio nuevo por ruta absoluta, cosa que nos pasó y produjo una ejecución que parecía entregada y correcta mientras su propia transcripción decía que toda escritura había sido rechazada.
Después escribe el juez antes de mirar ninguna salida, guarda el resultado esperado en un archivo, y termina con el cruce: copia cada merge.py entregado a un solo directorio, dales a todos la misma entrada nueva, e imprime las salidas una al lado de la otra. La discrepancia, si la hay, cabe en una pantalla.
Lo que esta medición no prueba
Esta medición no prueba una tasa. Tres ejecuciones de un modelo en una tarea es un estudio de caso, y el resumen honesto es que encontramos una clase de ambigüedad en una spec, no que los agentes de IA de programación discrepen en un N por ciento de las veces. Una tarea distinta, un modelo distinto o una spec con menos reglas interactuando podrían producir fácilmente tres programas idénticos, y no nos sorprendería.
Tampoco prueba que alguna de las dos lecturas esté equivocada. Las dos se defienden a partir de un documento que escribimos nosotros, y la culpa es nuestra, como autores de la spec, no de los agentes. Lo que la medición establece es más estrecho y, creemos, más útil: pasar un juez independiente de corrección no vuelve intercambiables a dos implementaciones, y la prueba del propio agente no puede cerrar esa distancia porque está escrita desde la misma lectura que escribió el código.
Una limitación que no pudimos quitar: nuestro juez solo comprueba los datos que la spec ejercita, así que aprueba a los tres por construcción. Un juez más estricto llevaría también los casos frontera, que es justo el consejo de este artículo, y nosotros solo escribimos la entrada frontera después de que las ejecuciones terminaran y discreparan. Hay además un problema más amplio con el que esto conecta y que esto no resuelve, descrito en Hacker News el 12 de febrero de 2026 por el usuario anotherCodder, que publicó un validador tras perder tiempo con "AI tool configs that are almost right but silently wrong". Un merge que resuelve la ambigüedad al revés es exactamente eso: válido, silencioso y no es lo que querías. Todas las mediciones de este artículo se tomaron el 20 de agosto de 2026 con los comandos mostrados.