Saltar a contenido

HT002 · Instalarlo en macOS y ponerlo en marcha

2026-09-07. La tarea cabía en una frase: clona cppide, instálalo en este macOS y ponlo en marcha. El resultado fue $38.24 / 4 pasos / cerca de 1 hora; el programa efectivamente arrancó en macOS (PID 96040, Mach-O arm64), y el veredicto fue inalcanzable, se elevó a la persona, y la persona lo aceptó.

Esta fue la primera run con goal guard —— todo lo que salió de las lecciones de HT001, estrenado ese mismo día contra la API real. Evitó un pozo en el que HT001 había caído y, a la vez, produjo un fallo nuevo exactamente en dirección contraria: convirtió git clone && make && ./cppide en una hora. La parte más valiosa de esta página es la segunda.

El registro original está en human-test/HT002/; las cuentas salen de runs/manifest.json y runs/sessions.db:

python tools/analyze_run.py human-test/HT002/runs

La forma de esta run

Ítem Valor
Tarea clonar cppide, instalarlo en este macOS y ponerlo en marcha, con una interfaz que la persona pueda abrir y usar de verdad
Coste $38.24 —— 4 pasos, desglose abajo
Duración cerca de 1 hora (0.97h en total)
Turnos 70 (suma de num_turns de los cuatro pasos)
Contexto del main thread 36.8K → 80.2K, con caída en picado en los límites de paso, no crecimiento monótono
Reparto 6 subagents; 7 llamadas a herramienta del coordinator frente a 212 de los subagents
Caché acierto del 90.8%
Workbench 5 artefactos, 4 notas, 11 ficheros de spill, 0 scripts reutilizables
Resultado arrancó (PID 96040, Mach-O arm64); veredicto inalcanzable, la persona lo aceptó
Versión la primera run con goal guard

Coste, turnos y duración de cada paso

Paso Coste Turnos (num_turns) Duración
Confirmar requisitos $0.5306 9 0.10h
Fijar objetivo $0.4117 12 0.05h
Trabajar $35.8950 12 0.73h
Trabajar·veredicto#1 $1.4037 37 0.09h
Total $38.2409 70 0.97h

La columna de duración es la suma de los valores de cada paso ya redondeados. Recalculando sobre el registro original, los cuatro duration_s de manifest.json suman exactamente 3471.53 segundos = 0.9643h; esta página mantiene 0.97h, con una diferencia de 0.006h.

Casi todo el dinero está en el paso «Trabajar»: $35.8950, el 93.9%. La primera sección explica en qué se fue ese dinero y por qué no debería haberse ido así.


1. Titular: convirtió git clone && make && ./cppide en una hora

Este es el hallazgo más importante de la run, más que todos los «mecanismos que funcionaron» de abajo.

En el paso de fijar objetivo, el judge se escribió a sí mismo un checklist de veredicto de 15 ítems (el texto está en 目标.md, bajo .flower/notes/). Clasificados por naturaleza:

Categoría Ítems Ejemplo
Verifican de verdad «que esto funciona» unos 5 build con exit 0; proceso vivo ≥30 segundos; el comando de reinicio se puede volver a ejecutar en una shell nueva
Verifican «que el proceso siguió las reglas» 6 los mtime de ~/.zshrc / .zprofile / .bash_profile son anteriores a esta sesión; no se ejecutó brew install / npm -g / sudo; el mtime de .flower/ no cambió y runs/ no fue alterado a mano
Imposibles de verificar por principio 4 tomar captura y mirarla; consola de devtools; clic manual; confirmación en persona

Solo un tercio verifica «si esto realmente se puede usar». Seis ítems verifican si él mismo siguió las reglas —— y uno de ellos comprueba el mtime del directorio .flower/, que es el directorio del propio framework.

Ese ítem sobre .flower/ se suspendió a sí mismo

No es un problema teórico. En la ronda de veredicto, el judge escribió honestamente entre los «no aprobados»:

Todos los ficheros bajo .flower/ tienen mtime anterior al inicio de la sesión —— fallo literal: INDEX.md 16:14, artifacts/01–05 (15:40–16:12), notes/决策-项目形态与验收路径.md 15:43 se escribieron todos dentro de la sesión.

La causa: el framework exige de forma rígida escribir los artefactos largos en .flower/artifacts/ y las decisiones en .flower/notes/. Y ese ítem del checklist exigía que ese directorio no se tocara. Dos reglas chocan de frente, y el choque lo fabricó el propio checklist —— el worker no escribió ni un solo artefacto largo y con eso habría cumplido mejor.

No lo maquilló: el paso de trabajar listó el conflicto por iniciativa propia en el informe final, y el registro de decisiones documenta cómo se trató —— ningún fichero existente se tocó (需求.md, 目标.md, 问答记录.md: los tres con mtime y MD5 idénticos antes y después), solo se añadieron nuevos. El judge, tras revisar, aceptó que «la manipulación que este ítem quería impedir no ocurrió», pero aun así lo listó como no aprobado según la regla y lo dejó en manos de la persona.

Causa raíz 1: los límites se convirtieron en ítems de veredicto

Los límites obtenidos en la fase de clarify («solo se puede instalar dentro del directorio del proyecto», «no tocar el código de negocio») restringían cómo trabajar, pero acabaron escritos como comprobaciones que verifican el artefacto. De ahí salen ítems como comprobar el mtime de .zshrc o el mtime de .flower/.

Límite ≠ ítem de veredicto. El límite restringe el proceso; el ítem de veredicto verifica el resultado. Una vez mezclados, cada límite añadido equivale a un ítem de veredicto más —— y los límites son justo lo que la fase de clarify anima a llenar: CLARIFIER_RULES dice explícitamente «deja claro qué NO se hace. Este apartado gobierna a todos los que trabajen después».

Contra el registro original se ve cada eslabón de esa cadena de amplificación: el límite del brief decía «no tocar el .flower/ y el runs/ ya existentes en el directorio actual», y en el checklist de veredicto se convirtió en «todos los ficheros bajo .flower/ tienen mtime anterior al inicio de esta sesión» —— de «no toques lo que ya existe» a «no puede haber ningún fichero nuevo en todo el directorio».

Causa raíz 2: JUDGE_RULES solo empuja en una dirección —— y eso se añadió ese mismo día

Esa mañana, a partir de las lecciones de HT001, se escribió esto en JUDGE_RULES:

  • «cada ítem tiene que poder verificarse en el momento», «lo que falte o sea ambiguo, complétalo tú»
  • «se juzga el artefacto, no el código fuente» (con el contraejemplo de macOS)
  • «los ítems que no se puedan verificar en este entorno, márcalos ahí mismo»
  • «primero mira dónde estás (puedes usar uname -a

Cada una empuja hacia más comprobaciones y más rigor. En toda la regla no hay una sola frase que diga «debe ser proporcional al tamaño de la tarea».

Es decir: optimizar contra el fallo de HT001 («declarar aprobado de más») produjo el fallo exactamente contrario: comprobar de más. Esto es de fabricación propia, no un problema del modelo.

Causa raíz 3: se sumó al issue #3

Lo que realmente bloqueaba eran dos defectos de portabilidad en macOS:

  • src/proc.cpp usa ::sigemptyset, y el SDK de Apple, después del prototipo de la función, la vuelve a definir como macro tipo función (protegida solo por #ifndef _ANSI_SOURCE), con lo que ::sigemptyset(&s) se expande a ::(*(&s)=0,0), error de sintaxis
  • src/ui.cpp usa ::_exit sin incluir <unistd.h>

Se arregla en tres líneas. Pero el usuario había respondido en la fase de clarify «no tocar el código de negocio», el agent lo ejecutó al pie de la letra y se fue por el desvío: probar flags de compilación, despachar subagents, escribir registros de decisión. Si en ese momento hubiera podido decir «cambiar esas dos líneas no pasa nada», se acaba en diez minutos. Un límite escrito como prohibición se interpreta con la máxima severidad, y a mitad de camino no hay ningún canal para aflojarlo.

En el registro original se ve lo largo que fue el desvío: cuatro puntos de bloqueo en total, de los cuales solo el de ::_exit se resolvía con flags de compilación puras; el de ::sigemptyset se intentó con 6 combinaciones de -D/-U y todas fallaron (la familia _POSIX_C_SOURCE además rompe de paso st_mtimespec y O_CLOEXEC). Ese callejón sin salida produjo por sí solo un informe de viabilidad de 13.6K.

Pero seamos justos: el límite forzó una solución mejor

La solución final no fue tocar el código fuente, sino generar en el Makefile una cabecera-shim que solo hace #undef y forzarla por delante con -include:

$(MACSHIM):
    @printf '#include <signal.h>\n#undef sigemptyset\n#undef sigfillset\n...' > $@
CPPFLAGS += -include $(MACSHIM) -include unistd.h

Esto es mejor que tocar el fuente —— el repositorio upstream no cambia ni una línea, y cualquiera que lo clone puede compilar. El shim se genera en build/ y no entra en el repo, clean recoge con rm -rf build, y make -n UNAME_S=Linux se comprobó de verdad: la expansión no lleva -include, la ruta de Linux queda sin contaminar. El git diff final toca un único fichero, Makefile: +14 / −2.

Así que el límite produjo una solución mejor a mayor coste. El problema no es que lo hiciera mal, sino que ese trade-off no se le presentó a la persona —— y la razón por la que no se pudo presentar es justamente el issue #3.

Por qué git status muestra 64 entradas y aun así el fuente tiene cero cambios

El repositorio de cppide tiene commiteados 30 .o, 30 .d y 2 binarios, y encima son ELF de Linux aarch64 con mtime posterior a los .cpp —— sin un make clean previo, make se salta la compilación y linka directamente los ELF, y revienta seguro. Por eso, de las 64 entradas de git status --porcelain, 63 son artefactos de compilación trackeados por git, más un ?? build/. Filtrando por extensión, git diff -- 'src/*.cpp' 'src/*.h' README.md config.sample.json | wc -c da 0. Esta formulación precisa la forzó la auditoría de cumplimiento —— el coordinator quería escribir en general «src/ sin cambios», y la auditoría señaló que esa frase no se sostiene.


2. Qué hizo bien el goal guard

Todo el lado del mecanismo funcionó, y los dos cambios de ese día dejaron huellas directas en el artefacto.

El primer ítem del checklist que escribió:

uname -a muestra Darwin 25.2.0 (macOS)… todos los ítems de «arranca / se abre» de abajo tienen que ser resultados obtenidos ejecutando de verdad en esta máquina, no se acepta inferencia del tipo «el fuente tiene una rama para macOS, así que debería funcionar»

Eso es literalmente lo que se escribió esa mañana en JUDGE_RULES a partir de las lecciones de HT001.

Usó tres veces la marca [no verificable en este entorno:…] añadida ese día, cada una con su razón —— consola de devtools (no hay navegador que se pueda dirigir, e instalar uno headless para eso cruzaría el límite), clic manual (requiere una persona delante de la pantalla), confirmación en persona (solo la puede dar él).

La primera frase del judge fue «no saco conclusiones de ese texto de respuesta. Voy al sitio.» Y entonces:

file cppide        → Mach-O 64-bit executable arm64
lsof -p 96040      → txt apunta a /Users/hechenyu/explore/test-ide/cppide/cppide
                     arrancado a las 16:10, a las 16:15 seguía vivo

Él mismo escribió: «el pozo de "entregar un ELF de Linux" de las lecciones meta, esta vez no se pisó. Un ELF de Linux no puede llegar a ser un PID en Darwin. Esto no se dedujo del Makefile.»

También comprobó que el fuente no se tocó: todos los .cpp / .h bajo src/ tienen mtime 15:32 (el momento del clone); lo único modificado es el Makefile (16:06), y los .o / .d (16:07) son artefactos de compilación trackeados, reconstruidos.

También declaró honestamente dónde no llegaba. El Bash del judge estaba restringido al directorio de trabajo: la comparación con git ls-remote y el mtime de ~/.zshrc no los podía ejecutar, solo podía dar por buena la salida de la auditoría independiente —— esos dos ítems los listó aparte indicando «no pude reejecutarlos con mis propias manos».

3. Veredicto «inalcanzable», y entonces preguntar a la persona

Veredicto de tres estados más intervención humana: la cadena completa funcionó de verdad por primera vez.

El veredicto fue inalcanzable, atascado en el ítem de la captura: tanto screencapture -x como -l <window-id> devuelven could not create image from display —— no está concedido el permiso TCC de grabación de pantalla, y concederlo exige que una persona lo pulse en Ajustes del sistema, lo cual a su vez es un cambio del sistema prohibido por el límite.

«Despachar otra ronda de subagents no va a hacer aparecer esa imagen. Según las reglas no se puede dar por aprobado, y tampoco debe ser "aún no": es sencillamente inalcanzable, hay que parar y pasárselo a la persona.»

Con eso, los ítems «imposibles de verificar por principio» pasaron de 3 a 4 —— 4 de los 15 no se pueden verificar en absoluto en este entorno, y eso se descubrió después de gastar $35.8950 en trabajar y $1.4037 en el veredicto. El paso de fijar objetivo costó solo $0.4117: se tendría que haber descubierto ahí.

El framework le planteó esta conclusión a la persona (queda en el registro de preguntas, q7), con tres opciones; la persona eligió «acepto este resultado», el gate dio paso y el workflow terminó.

También rechazó un sustituto. El worker leyó con AppleScript el texto de la ventana del terminal: era la TUI realmente renderizada —— la columna de números de línea, el cuerpo de src/main.cpp, la barra de paneles ─[编译]─[运行]─[AI*]─[输入]─, la barra de estado 练习模式 │ main.cpp │ 1:1 │ C++; ni pantalla en blanco ni pantalla de error. El judge dijo:

«El que trabajó no lo hizo pasar por "mirar la captura"; esa autocontención es correcta. Pero no sustituye al ítem que pide el checklist, y yo tampoco se lo voy a dar por bueno.»

«Revelar el riesgo» no equivale a aprobado

En esto cayó HT001: escribió «nunca se ejecutó en macOS» en la nota de entrega y aun así dio por aprobado el criterio 1. En HT002 el judge tenía una evidencia sustitutiva mejor (texto real de la TUI) y aun así se negó a usarla para tapar el ítem del checklist. Esa diferencia es todo el sentido del diseño de tres estados del veredicto —— UNREACHABLE no es un eufemismo de NOT_YET, significa «hay que parar y preguntar a la persona».

4. allowed_tools no es una whitelist dura

Al analizar esta run apareció un hecho que contradice lo que se afirmaba. Desglosando las llamadas a herramienta por sesión:

Sesión Lo que usó de verdad Qué había en la whitelist
Confirmar requisitos WebFetch, Glob, ask×6 clarify() daba entonces solo ask + Read/Glob/Grep
Fijar objetivo Bash ×11 judge() por defecto can_run=False, sin Bash
Trabajar·veredicto Bash ×31, WebFetch ídem
Trabajar (coordinator) Bash ×1, Agent ×6 correcto

goal_step no tiene ninguna ruta de código que pase can_run, así que la fila de «fijar objetivo» no puede ser un problema de configuración.

Sonda de verificación de $0.1: a un agent con allowed_tools=["Read"] se le pide escribir un fichero ——

Write → capa de permisos: "requested permissions to write ... but you haven't granted it yet"
Bash  → seguridad de rutas: "Output redirection was blocked. For security, Claude Code may
        only write to files in the allowed working directories"

El modelo puede invocar herramientas que no están en la whitelist, solo que las frena la capa de permisos y la seguridad de rutas. Por tanto:

  • allowed_tools es una lista de exención de aprobación, no una whitelist excluyente
  • lo que entonces protegía de verdad a clarify() / judge() era el permission_mode="default" heredado
  • y ambos tenían delegate_only=Falsesin el hook delegate_guard. El coordinator está cubierto por hooks; estos dos roles no tenían entonces ningún mecanismo propio de flower

El docstring de clarify() decía «no se le dan herramientas de escritura —— no puede ponerse a trabajar», y aquel contraexperimento de $0.8908 se hizo entero por esa frase. La base mecánica de esa frase es falsa.

Los dos hechos de esta sección han cambiado desde entonces

whitelist_guard ya está conectado incondicionalmente: a los roles con delegate_only=False el Runtime les instala ahora ese hook automáticamente, y clarify() / judge() ya no están «sin ningún mecanismo». Además WebFetch está hoy en la whitelist de clarify(), con lo que el ejemplo de la primera fila de la tabla queda invalidado —— la conclusión se mantiene, pero ahora se apoya solo en la evidencia de la sonda de $0.1. Ver la última sección de esta página.

5. Curva de contexto: los límites de paso resetean

Turno 1     36.8K
Turno 25    80.2K   ← pico
Turno 26    32.9K   ← caída: paso nuevo = sesión nueva (resume_from=None)
Turno 121   64.8K

Tres de los cuatro puntos de muestreo (turnos 1 / 25 / 121: 36.8K / 80.2K / 64.8K) coinciden uno a uno con el registro original; el 32.9K del turno 26 no cuadra al recalcularlo: reproduciendo los mensajes assistant del main thread ordenados por store_key, el 26.º (el primero de la sesión nueva) suma input + cache_read + cache_creation = 31,972 tokens = 32.0K, mientras que 32.9K es el segundo valor de esa misma sesión (31,970 + 948). Esta página conserva 32.9K —— la diferencia es de 0.9K, y la conclusión de «caída en picado» se sostiene con cualquiera de los dos valores.

HT001 subió monótonamente hasta 185.9K —— un solo paso corriendo 10 horas. HT002, al partirse en cuatro pasos y abrir sesión nueva en cada uno, resetea el contexto de forma estructural. Es el efecto directo del diseño «el brief y el objetivo son piezas congeladas; aguas abajo se recibe el documento, no la conversación» —— se ve en la curva.

Acierto de caché del 90.8% (HT001 fue 96.1%). La tasa de acierto sube con la longitud de la sesión, así que las sesiones cortas salen algo más caras por unidad —— es la otra cara de la misma moneda que la conclusión de la sección 3 de HT001.

6. Comparación con HT001

HT001 HT002
Tarea escribir un IDE de terminal desde cero instalarlo en macOS y ponerlo en marcha
Coste / duración $171.62 / 10.44h $38.24 / 0.97h
Pasos 2 (sin goal guard) 4 (con fijar objetivo + veredicto)
subagents 23 6
Llamadas a herramienta del coordinator 32 7 (6 de ellas Agent)
Llamadas a herramienta de subagents 1,893 212
Contexto del main thread 28.7K → 185.9K, monótono 36.8K → 80.2K, reset en límites de paso
Acierto de caché 96.1% 90.8%
Scripts del workbench 58, ejecutados 331 veces, 92% reutilizados 0
Veredicto ninguno (autoauditoría espontánea del coordinator) veredicto de tres estados, «inalcanzable», se pregunta a la persona

La última fila merece atención. HT002 no escribió ni un script reutilizable —— la tarea duraba una hora, no había nada que valiera la pena sedimentar. Esto indica que el valor del workbench crece con la longitud de la tarea; en tareas cortas es puro coste.

Qué aspecto tiene el registro original

.flower/INDEX.md es la instantánea del workbench al terminar esta run, y se puede mirar directamente:

Directorio Contenido
scripts/ 0
artifacts/ 5 —— 01-recon.md (32.6K), 02-build-run.md (19.4K), 03-flag-only-feasibility.md (13.6K), 04-compliance-audit.md (16.4K), 05-final-build-run.md (19.8K)
notes/ 4 —— 需求.md, 目标.md, 问答记录.md son las tres piezas congeladas que escribe el framework; de registro de decisiones propio del agent solo hay 1
spill/ 11 ficheros de spill

Bajo runs/ están manifest.json (4 registros de paso) y sessions.db. En la base de datos hay 10 sesiones: 4 del main thread más 6 de subagents —— cuadra con «6 subagents».

En el registro de preguntas hay 7 en total: q1–q6 son de la fase de clarify (cuadran con las 6 llamadas a ask del paso de confirmar requisitos), y q7 es la puerta humana posterior al veredicto «inalcanzable».

Qué cambió esta run en el framework

Hallazgo Cambio implementado
De 15 ítems de veredicto, solo unos 5 verifican si funciona (sección 1) Se añadió presión en sentido contrario a JUDGE_RULES: la longitud del checklist la decide el número de formas de fallar, no el grado de rigor, y se metió este caso como contraejemplo en el texto de la regla («una tarea de "instalar y arrancar" acabó con 15 ítems, y solo 5 verificaban si funciona» —— la regla redondea a 5; el criterio de clasificación de la sección 1 de esta página dice «unos 5»)
Los límites se escribieron como ítems de veredicto JUDGE_RULES lo fija directamente con una frase: «un límite no es un ítem de veredicto»
4/15 ítems no verificables en este entorno, descubierto tras gastar $35.8950 + $1.4037 El paso de fijar objetivo exige añadir en el momento el sufijo [no verificable en este entorno: razón] a los ítems no verificables —— esta vez lo aplicó bien en 3 y se dejó el de la captura
clarify() / judge() sin protección de hooks (sección 4) whitelist_guard ya está conectado incondicionalmente: a los roles con delegate_only=False se lo instala el Runtime, sin depender de si el workbench está activado
Efecto colateral: el judge de fijar objetivo ya no dispone de Bash Con whitelist_guard en su sitio, si no se pasa explícitamente can_run=True, el ítem de JUDGE_RULES que dice «primero uname -a para ver dónde estás» no se puede ejecutar —— es un trade-off nuevo, aparecido tras el arreglo
La base mecánica del docstring de clarify() es falsa Corregido. WebFetch también entró en la whitelist de clarify(), así que ese ejemplo queda invalidado y la conclusión pasa a colgar de la sonda de $0.1
No hay canal para meter una frase a mitad de camino (causa raíz 3 de la sección 1) Registrado como issue #3, sin cerrar —— esta vez se sumó directamente a la sobrecomplicación, y un «cambiar esas dos líneas no pasa nada» habría ahorrado una hora

Resumen de esta run en una frase: optimizar contra el fallo de la vez anterior produce con mucha facilidad un fallo nuevo en dirección contraria. HT001 le enseñó al framework «no des por aprobado a la ligera»; HT002 demostró de inmediato lo que pasa cuando esa regla no viene acompañada de «no compruebes de más». Las dos tienen que estar en JUDGE_RULES: falta una y se tuerce.