HT001 · Escribir un IDE de terminal desde cero¶
El 2026-09-07, un agente escribió desde cero, bajo flower, un IDE de un solo archivo para C/C++ que corre en la terminal de macOS. Corrió 10.4 horas seguidas, gastó $171.62 y entregó 12,212 líneas de código de producto y 13,291 de pruebas, con una caída de red a mitad de camino de la que se recuperó solo. Esta página no es una vitrina de resultados: es medir cada afirmación del framework a escala real, incluidas las dos que no se sostuvieron: que el coordinador "ignora frecuentemente las restricciones de permisos" es falso, y en su propia verificación de aceptación hay un criterio que juzgó mal.
El registro completo (incluido un transcript de 19MB) está en ChenyuHeee/cppide. Cada número de abajo lo calcula tools/analyze_run.py a partir de sessions.db, y es reproducible:
De dónde salen los números de esta página
El runs/ de HT001 está en ese repo de cppide, no en el repo de flower. Así que los números de esta página están copiados tal cual del registro del run de entonces; al escribirla no se volvió a recalcular contra sessions.db —— para recalcular hay que clonar cppide primero y luego correr el comando de arriba. HT002 es al revés: el registro original está en este mismo repo, y los números de esa página se recalcularon uno por uno, con las dos discrepancias escritas directamente en la página.
La forma de este run¶
| Ítem | Valor |
|---|---|
| Tarea | "Hacer un IDE de un solo archivo para C/C++ en la terminal de macOS, para entrenar ICPC" |
| Costo | $171.62 — clarificar requisitos $0.3704 / 5 turnos, trabajo $171.2476 / 31 turnos |
| Duración | 0.06h + 10.44h |
| Contexto del main thread | 28.7K → 185.9K, crecimiento monótono a lo largo de 70 turnos de modelo, sin compact en todo el run |
| Reparto | 32 llamadas a herramientas del coordinador vs 1,893 de los subagents; el 94.8% de los caracteres de contenido cae en los subagents |
| Caché | Entrada total 299.4M tokens, acierto 96.1% |
| Producto | src/ 12,212 líneas, tests/ 13,291 líneas (unas 3,000 aserciones), README 44K |
| Versión | flower antes de añadir el goal guard, así que este run no tiene verdict por turno |
Esta página tiene dos "turnos" distintos
Los 5 turnos / 31 turnos de la tabla son el num_turns registrado en runs/manifest.json, un número por step; el "turno 70" de la curva de contexto de abajo cuenta turnos de modelo en el main thread, es decir cada mensaje del assistant. No cuentan lo mismo; no los conviertas entre sí.
1. Economía del contexto: el 94.8% del terreno nunca entró al main thread¶
Esta es la afirmación central del framework: el coordinador solo carga decisiones, el terreno se hunde hacia los subagents y el disco. El mecanismo está en Economía del contexto.
| Main thread | subagent | % subagent | |
|---|---|---|---|
| Turnos de modelo | 70 | 3.0K | 97.7% |
| Caracteres de contenido | 200.1K | 3.6M | 94.8% |
| Llamadas a herramientas | 32 | 1,893 | —— |
Este run despachó 23 subagents, con un máximo de 184,377 tokens de salida, mediana 72,197 y mínimo 11,684.
De las 1,893 llamadas a herramientas que tocaron algo (1,034 Bash / 471 Read / 258 Edit / 126 Write), solo 32 entraron en el campo de visión del coordinador — 59:1. Por cada despacho, hay en promedio 82 llamadas a herramientas que no ve.
Las pruebas pequeñas anteriores midieron 83%; a escala real es 94.8%. Cuanto mayor la escala, mayor el rendimiento del reparto, porque el costo del task brief y de la respuesta es fijo por despacho, mientras que el terreno que queda fuera crece con la complejidad de la tarea.
2. Curva de contexto: por primera vez sabemos dónde está el techo de "long-horizon"¶
El input + cache_read + cache_creation de cada mensaje del assistant es el contexto que el modelo vio en ese turno:
Turno 1 28.7K ← piso de arranque (medido antes en ~34k, cuadra)
Turno 20 35.2K
Turno 35 108.6K
Turno 50 158.2K
Turno 70 185.9K ← fin
- Crecimiento monótono durante 70 turnos, pendiente de unos 2.2K/turno
- Nunca ocurrió un compact (
DISABLE_AUTO_COMPACT=1; la única bajada es el registro vacío del turno con caída de red) - Se usó el 18.6% de la ventana de 1M
Extrapolando con esa pendiente, el muro está en unos 440 turnos. Estos 70 turnos produjeron 12K líneas de código, así que el techo real de lo long-horizon en la forma actual es unas 6 veces este run. Es un número con el que se pueden tomar decisiones; antes solo se podía adivinar.
Para superarlo hay que hacer que el crecimiento del main thread sea sublineal: más al disco, menos en la conversación.
3. Acierto de caché 96.1% — los $171.62 solo se sostienen por eso¶
Entrada total 299.4M tokens
acierto de caché 287.5M (96.1%)
caché creada 11.8M
sin cachear 6.2K
Salida 2.3M (de los cuales thinking 653.9K)
299M tokens de entrada costaron solo $171 porque el 96% fue a precio de caché.
Corolario: la viabilidad económica de un run long-horizon descansa en la tasa de acierto de caché, y esa tasa depende de la estabilidad del prefijo del contexto. Cualquier optimización que "reordene el contexto" —— mover contenido viejo, insertar un system prompt nuevo por delante —— rompe la caché, y los tokens ahorrados difícilmente compensan el descuento perdido. Esto no se había medido antes; ahora hay número.
Es también una razón colateral por la que flower usa handoff en vez de compact: el handoff abre una sesión nueva y reconstruye la caché desde cero, y dentro de una generación el prefijo se mantiene estable.
4. Reutilización del workbench: 61 scripts, ninguno de usar y tirar¶
La afirmación del workbench es "escribes el script una vez, luego lo ejecutas, no lo reescribes". La verificación no es preguntarle al agente, sino contar en el transcript cuántas veces los scripts de .flower/scripts/ fueron escritos con Write/Edit y ejecutados con Bash:
61 scripts: escritos 95 veces, ejecutados 331 veces
Con más de 1 ejecución (realmente reutilizados): 56, el 92%
Escritos y jamás ejecutados: 0
Ratio ejecución/escritura 3.48
wave5-verify.sh se escribió 5 veces y se corrió 45 —— pulido iterativo y luego uso repetido, exactamente la forma deseada. Ningún script fue de usar y tirar.
5. El coordinador fue denegado 1 vez, y la culpa fue de la whitelist¶
La sospecha previa era: "el coordinador ignora frecuentemente las restricciones de permisos y dispara un montón de llamadas denegadas". La medición tumbó esa premisa.
De 32 llamadas a herramientas, 1 fue denegada (3.1%), y esa una fue:
which g++ clang++ make pkg-config 2>&1; echo ---; ls /usr/include/ncurses.h 2>&1; echo ---; ls /work/runs
Juicio segmento por segmento: echo permitido, ls permitido, which bloqueado —— no está en la tabla de comandos efímeros.
El comando entero es de solo lectura, un sondeo estándar del entorno. El modelo hizo exactamente lo correcto; la whitelist estaba incompleta, y lo forzó a pagar el arranque de un subagent (unos 4.3k tokens) por un which g++.
Ya corregido: se añadieron which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers. command y type solo se permiten en forma de consulta —— un command rm -rf / pelado es ejecución, y omitir ese -v equivale a abrir una puerta trasera; hay 7 casos adversariales en tests/glance.py custodiándolo.
Lo que más vale recordar de este punto no es el parche, sino el método. Una discusión de arquitectura que duró mucho tiempo tenía como premisa de partida algo que en datos reales es 1/32, y con la atribución invertida: el problema no era el modelo, era la whitelist. Primero medir.
6. Recuperación tras caída de red: primera validación con un fallo real¶
01:52:40 tool_result: "Agent terminated early due to an API error" ← un subagent interrumpido
01:55:41 assistant: "API Error: ENOTFOUND" ← lo recibe el main thread
↓ la sonda queda esperando a que vuelva la red
resumed=True, continúa la misma sesión y corre 8 horas más hasta terminar
El manifest registra attempts=2 / resumed=True / ok=True. 10 horas de trabajo no se rehicieron desde cero. Hasta entonces, la resiliencia seguía marcada como "aún sin verificar": cortar la red requiere sudo y editar hosts, y nadie quiere hacer eso durante un run real.
Ese mensaje sintético de "API Error" fue retirado por PruningSessionStore en la carga, así que el modelo nunca lo vio —— el diseño del pruning quedó también validado por un fallo real.
Dos problemas que quedaron expuestos (ver issue #2):
- La recuperación ocurre en la capa del framework, no en la de transporte. El costo es reproducir todo el contexto de nuevo, y ese contexto era grande.
- El subagent interrumpido perdió su trabajo a medias. El resume salva el main thread; la recuperación de interrupciones a nivel de subagent es otro problema.
7. ¿Su autoverificación es real?¶
Este es el punto que más hay que sospechar: un agente que escribe sus propias pruebas, las corre y declara que pasan, degenera fácilmente en trámite.
Después se despachó un subagent auditor independiente que leyó estáticamente los 58 scripts y los 22 artefactos. La conclusión es que es real, y con un rigor poco común:
- 0 aserciones tautológicas. En todo el repo no hay patrones tipo
[ 1 = 1 ]. - 8 apariciones de
|| true, 7 legítimas (grep -cdevuelve 1 sin coincidencias, fallback deulimit, etc.). - Se juzga comportamiento, no existencia de archivos.
audit-idle-cost.sh:53deja el IDE inactivo 60 segundos en un pty, cuenta las peticiones recibidas por un servidor falso local, y solo hay PASS con-le 2.fix-p1-idle-verify.sh:100-110va más lejos: asegura que en reposo se llama exactamente 1 vez y que el ghost sigue mostrándose —— para evitar que "el cambio de limitación de tasa se lleve por delante la funcionalidad". - Levanta procesos hijos de verdad.
wave5-pty-drive.cpp:316usaforkptypara correr la TUI de verdad;audit-fake-ai-server.pyes un servidor de sockets real, con cinco inyecciones de fallo/err500 /err401 /badjson /drop /slow; todas las rutas de IA fuerzan el vaciado deCPPIDE_API_KEY.
Dos cosas superaron lo esperado.
Le puso control positivo y negativo a su propio verificador. 验收审计.md:29-34: primero alimenta ZZZ_NEVER_APPEARS_ZZZ para confirmar que el driver da FAIL, luego una cadena real para confirmar que da PASS —— evitando "auditar con una máquina falsa que siempre pasa". Eso es verificar al verificador.
Divulgó por iniciativa propia sus aserciones débiles. El for m in $must de wave5-b4-ai-states.sh:114 parte "AI 错误 请求超时" por espacios en tres subcadenas y hace grep de cada una, pero AI y 错误 aparecen inevitablemente en cualquier pantalla —— nominalmente "3 criterios pasan", en la práctica solo 1 discrimina. Esto lo escribió él mismo en 验收审计.md:576-582, no lo destapó el auditor.
En la misma línea: wave5-verify.sh:296-302 dedica 7 líneas de comentario a explicar por qué B3b se degradó a SKIP (aserción de temporización de la última pantalla, sin relación con el código de producto), anotando que "la lógica de juicio y el margen de tolerancia no se tocaron ni una letra"; :236-238 registra que encontró y arregló una aserción falsa que él mismo había escrito antes —— if pgrep ... cae silenciosamente al else en imágenes reducidas sin pgrep, quedando siempre en PASS.
La evidencia cualitativa de reutilización coincide con los números de la sección 4: audit-fake-ai-server.py es reutilizado por 7 scripts; wave5-pty-drive.cpp se reutiliza en 5+ compilaciones, con caché de compilación vía [ -x "$DRIVE" ] ||; el driver de wave4 → wave5 es una extensión, no una reescritura (el diff cambia 1 línea y añade 343; las 281 líneas de wave4 quedan intactas).
Todos estos comportamientos ocurrieron en una versión sin goal guard. La única restricción eran los 10 criterios de aceptación decidibles del brief. Eso indica que escribir el "terminado" en forma decidible ya induce por sí solo autoauditoría y autodivulgación. El valor marginal del goal guard hay que medirlo contra esa línea base, no contra cero.
Pero la sección siguiente muestra: la autoauditoría es muy fuerte en técnica de verificación, y aun así falló el veredicto final del criterio más importante.
El "auditor" de aquí no es el judge
El auditor independiente de las secciones 7 y 8 es un subagent que el coordinador despachó por iniciativa propia, no el judge del goal guard —— este run no tenía goal guard en absoluto. Se leen parecido, pero hacen cosas distintas: el auditor es una revisión posterior, el judge es la puerta de verdict al final de cada turno.
8. ¿Es correcta la nota que se puso a sí mismo? El criterio 1 lo juzgó mal¶
Se despachó un segundo auditor independiente para juzgar esos 10 criterios de aceptación uno a uno contra el código.
Los criterios 2–10 son todos ciertos, con evidencia en el código. El modo práctica sin autocompletado es incluso una garantía estructural: único punto de decisión sink ai.h:117 + único switch de aterrizaje app.cpp:590-615 + cuatro early-returns en editor.cpp:679-683, no ifs dispersos. Los P1–P4 que encontró en su autoauditoría son todos ciertos y todas las correcciones están aplicadas (P4 medio cierto: esa línea del pair azul es código muerto, ghostAttr() ya usaba A_DIM).
Pero el criterio 1 lo juzgó mal.
Texto del criterio: "Compilar un ejecutable independiente que corra directamente en la terminal de macOS"
Artefacto entregado: ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux
Su veredicto: aprobado
Este punto lo verificó una persona corriendo file a mano.
No lo ocultó —— el título de la §4 de 交付说明.md es literalmente "El mayor riesgo no verificado: nunca se corrió en macOS", y la §6 de la auditoría lista seis superficies de macOS sin verificar. Divulgó el riesgo, pero nunca reconectó ese hecho con el veredicto del criterio 1, y el criterio 1 exige literalmente justo eso.
Pero esto no es evidencia de que "haga falta un judge independiente"¶
Es fácil leerlo de corrido como "entonces hace falta un goal guard". Es incorrecto, y leerlo así es disparar la flecha y pintar la diana después —— exactamente el razonamiento motivado que se acaba de criticar en la sección anterior.
La versión del goal guard de entonces, con su configuración por defecto, muy probablemente también habría dejado pasar este criterio. judge() tiene can_run=False por defecto, y sus únicas herramientas son Read / Glob / Grep —— no puede correr file. Habría leído el código fuente, habría visto que Makefile:25-38 sí tiene una rama Darwin, y habría dictaminado "cumplido". Exactamente el mismo fallo que la autoauditoría.
El hecho estructural de verdad¶
El criterio 1 era sencillamente inverificable en el entorno de este run.
Afirma algo sobre "el comportamiento del artefacto en macOS", y todo el run ocurrió dentro de un contenedor Linux. Ningún judge puede verificar un binario de macOS dentro del contenedor, sea autoauditoría o independiente. El problema no es "quién juzga", sino que este criterio se trató como un criterio decidible normal, y luego se juzgó.
Otros tres hallazgos del auditor (ninguno mencionado en la autoauditoría)¶
- Semántica de cuota de
doTab():app.cpp:909llama incondicionalmente anoteActivity()después deacceptGhost()→gen_++→ reabre la cuota, de modo que cada Tab aceptado dispara otra petición automática 500ms después. El análisis de P1 (quemar dinero en reposo) solo revisó los tres puntos de cancelaciónsetMode/cancelInFlight/askNow, y se saltó esta ruta de gasto, la más frecuente. - El "gris" del criterio 6 solo se cumple literalmente en terminales de 256 colores; en el nivel de 8 colores está fijado a
A_DIM(atenuado, no gris). - El subtítulo de la §7 de
验收审计.mddice "9 criterios de aceptación" pero la tabla tiene 10 filas —— errata, pero aparece en el encabezado de la tabla de evaluación global.
9. Escala final de spill y workbench¶
103 spills, 791.4K caracteres convertidos en punteros a rutas, sin residir en el contexto.
.flower/ al final: 58 scripts (250K), 22 artefactos (330K), 5 notas (43K).
Riesgos pendientes¶
El objetivo de entrega es macOS, pero toda la verificación se hizo dentro de un contenedor Debian aarch64; cero verificación en macOS real. Es el efecto secundario real del esquema de aislamiento por contenedor: para asegurarlo se lo encierra en Linux, y lo que tiene que entregar es macOS. El propio README §10.24 del proyecto registra esto con honestidad y lista 4 puntos de riesgo, pero no lo cierra.
Esa brecha la cubrió después HT002 —— ese run consistió precisamente en instalar este binario en una macOS real.
Beneficio inesperado: dentro del contenedor solo se ve /work, así que en los 19MB de transcript hay cero filtraciones del nombre de usuario del host /Users/hechenyu (/work aparece 11,495 veces). El aislamiento aisló las rutas de paso.
Lo que este run no logró verificar¶
- Goal guard: este run es anterior a su incorporación. El coordinador organizó una auditoría independiente por iniciativa propia, con técnica de verificación muy sólida, pero juzgó mal el criterio más importante (sección 8). Hay que quedarse con las dos conclusiones a la vez —— escribir criterios decidibles en el brief ya induce autoauditoría, así que la línea base del goal guard no es cero; y el goal guard con la configuración por defecto de entonces también habría dejado pasar ese criterio, así que tampoco es una panacea.
- Comportamiento con ventana de 1M: solo se usó el 18.6%, no se tocó el límite.
- Comportamiento del microcompact bajo
DISABLE_AUTO_COMPACT=1: nunca se disparó un compact en todo el run, así que sigue siendo inferencia.
Qué cambió este run en el framework¶
| Hallazgo | Cambio aplicado |
|---|---|
which bloqueado (sección 5) | is_ephemeral() añade which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers; tests/glance.py suma 7 casos adversariales que custodian la forma de consulta de command / type |
| Criterio 1 mal juzgado (sección 8) | El punto 1 de JUDGE_RULES pasa a juzgar el artefacto, no el código fuente —— si se puede ejecutar, se ejecuta; si no, hay que inspeccionar el artefacto en sí (plataforma, tamaño, si carga) |
| "Ya divulgué el riesgo" se convirtió en un aprobado | El veredicto debe distinguir "no logrado" de "aquí no se puede verificar". Verdict.parse ahora reconoce 无法验证 / 没法验证 / 无法判定 como un tercer estado unreachable → escala a la persona, nunca puede dictaminar aprobado |
| El criterio 1 era inverificable por principio en ese entorno | El paso de fijar el objetivo exige marcar en el momento los ítems que este entorno no puede verificar, en vez de descubrirlo al terminar el trabajo |
| Recuperación de red en la capa del framework, trabajo a medias del subagent perdido (sección 6) | Registrado como issue #2, sin cerrar |
Los tres primeros llegaron por primera vez a una API real en HT002, donde "juzgar el artefacto, no el código fuente" salvó la situación una vez —— pero esa misma regla también generó un fallo nuevo.