HT001 · Escrever um IDE de terminal do zero¶
2026-09-07, um agent escreveu do zero, sob o flower, um IDE de arquivo único para C/C++ rodando no terminal do macOS. Ele rodou 10.4 horas seguidas, gastou $171.62, entregou 12,212 linhas de código de produto e 13,291 linhas de teste, caiu a rede uma vez no meio do caminho e ele mesmo retomou até o fim. Esta página não é uma vitrine de resultados — é medir cada afirmação do framework em escala real, incluindo as duas que não se sustentaram: é falso que o coordenador "frequentemente ignora as restrições de permissão", e na verificação que ele mesmo fez de si mesmo há um item julgado errado.
O registro completo (incluindo 19MB de transcript) está em ChenyuHeee/cppide. Cada número abaixo é calculado por tools/analyze_run.py a partir de sessions.db, e é reproduzível:
De onde vêm os números desta página
O runs/ do HT001 está naquele repositório cppide, não no repositório do flower. Portanto os números desta página foram transportados tal como estavam do registro de execução da época; ao escrever esta página não houve um novo recálculo em cima de sessions.db — para recalcular, é preciso primeiro clonar o cppide e depois rodar o comando acima. HT002 é o inverso: o registro original está neste repositório, e os números daquela página foram recalculados item a item, com os dois pontos divergentes escritos diretamente na página.
O formato desta run¶
| Item | Valor |
|---|---|
| Tarefa | "Fazer um IDE de arquivo único para C/C++ no terminal do macOS, para treino de ICPC" |
| Custo | $171.62 — clarify $0.3704 / 5 turnos, execução $171.2476 / 31 turnos |
| Duração | 0.06h + 10.44h |
| Contexto da main thread | 28.7K → 185.9K, crescimento monótono ao longo de 70 turnos de modelo, sem nenhum compact |
| Divisão de trabalho | coordenador com 32 chamadas de ferramenta vs subagent com 1,893; 94.8% dos caracteres de texto caíram nos subagents |
| Cache | 299.4M tokens de entrada no total, 96.1% de acerto |
| Saída | src/ com 12,212 linhas, tests/ com 13,291 linhas (cerca de 3,000 asserções), README de 44K |
| Versão | flower antes do goal guard, então esta run não tem verdict por turno |
Esta página tem dois "turnos" diferentes
Os 5 turnos / 31 turnos da tabela são o num_turns registrado em runs/manifest.json, um número por step; o "turno 70" da curva de contexto abaixo conta os turnos de modelo na main thread, ou seja, cada mensagem do assistant. Não são a mesma coisa; não converta um no outro.
1. Economia de contexto: 94.8% do trabalho de campo não entrou na main thread¶
Esta é a afirmação central do framework — o coordenador só carrega decisões, o trabalho de campo afunda para os subagents e para o disco. O mecanismo em si está em Economia de contexto.
| Main thread | subagent | Fatia do subagent | |
|---|---|---|---|
| Turnos de modelo | 70 | 3.0K | 97.7% |
| Caracteres de texto | 200.1K | 3.6M | 94.8% |
| Chamadas de ferramenta | 32 | 1,893 | — |
Esta run despachou 23 subagents, com saída máxima de 184,377 tokens, mediana de 72,197 e mínima de 11,684.
Das 1,893 chamadas de ferramenta que puseram a mão na massa (1,034 Bash / 471 Read / 258 Edit / 126 Write), só 32 entraram no campo de visão do coordenador — 59:1. Em média, a cada despacho, há 82 chamadas de ferramenta que ele não vê.
Um teste anterior em pequena escala mediu 83%; em escala real são 94.8%. Quanto maior a escala, maior o ganho da divisão de trabalho — porque o custo do task brief e da resposta é fixo por despacho, enquanto o trabalho de campo barrado do lado de fora cresce com a complexidade da tarefa.
2. A curva de contexto: pela primeira vez sabemos onde fica o teto do "long-horizon"¶
O input + cache_read + cache_creation de cada mensagem do assistant é o contexto que o modelo viu naquele turno:
- 70 turnos de crescimento monótono, inclinação de cerca de 2.2K/turno
- Nenhum compact aconteceu durante toda a run (
DISABLE_AUTO_COMPACT=1; a única queda é o registro vazio do turno em que caiu a rede) - Consumiu 18.6% da janela de 1M
Extrapolando por essa inclinação, a parede fica por volta do turno 440. Estes 70 turnos produziram 12K linhas de código, então o teto real do long-horizon na forma atual é aproximadamente 6 vezes esta run. Esse é um número que dá para usar em decisões; antes só dava para chutar.
Para romper esse teto, o crescimento da main thread precisa ser sublinear — mais coisa afundando no disco, menos ficando na conversa.
3. 96.1% de acerto de cache — é só por isso que $171.62 se sustenta¶
299M tokens de entrada custaram só $171 porque 96% pagaram preço de cache.
Corolário: a viabilidade econômica de uma run long-horizon se apoia na taxa de acerto de cache, e essa taxa depende da estabilidade do prefixo do contexto. Qualquer otimização que "reorganize o contexto" — tirar conteúdo antigo, inserir um novo trecho de system prompt lá na frente — fura o cache, e os tokens economizados podem não compensar de longe o desconto de cache perdido. Isso nunca tinha sido medido; agora tem número.
Essa também é uma razão colateral pela qual o flower usa handoff em vez de compact: o handoff abre uma sessão nova e reconstrói o cache do zero, e o prefixo permanece estável dentro de uma geração.
4. Reuso da workbench: 61 scripts, nenhum escrito e jogado fora¶
A afirmação da workbench é "escreva o script uma vez, depois é só rodar, sem reescrever". A forma de verificar não é perguntar ao agent, é contar quantas vezes os scripts em .flower/scripts/ foram alvo de Write/Edit e quantas vezes foram executados via Bash no transcript:
wave5-verify.sh foi escrito 5 vezes e rodado 45 vezes — polido iterativamente e depois usado repetidamente, exatamente o formato desejado. Nenhum script foi escrito e jogado fora.
5. O coordenador teve 1 ação negada, e a culpa foi da whitelist¶
A suspeita antes de medir era: "o coordenador frequentemente ignora as restrições de permissão e dispara um monte de chamadas negadas". A medição derrubou essa premissa.
De 32 chamadas de ferramenta, 1 foi negada (3.1%), e essa foi:
which g++ clang++ make pkg-config 2>&1; echo ---; ls /usr/include/ncurses.h 2>&1; echo ---; ls /work/runs
Julgamento segmento a segmento: echo liberado, ls liberado, which barrado — não está na tabela de comandos efêmeros.
O comando inteiro é puramente somente-leitura, uma sondagem de ambiente padrão. O modelo fez tudo certo; a whitelist é que estava incompleta, forçando-o a pagar o custo de inicializar um subagent (cerca de 4.3k tokens) por causa de um which g++.
Já corrigido: foram adicionados which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers. command e type só são liberados na forma de consulta — um command rm -rf / pelado é execução, e esquecer aquele -v equivale a abrir uma porta dos fundos; há 7 casos adversariais em tests/glance.py guardando isso.
O que mais vale registrar aqui não é o patch, é o método. Uma discussão de arquitetura que durou muito tempo tinha como premissa inicial algo que nos dados reais é 1/32, e ainda com a atribuição invertida — o problema não estava no modelo, estava na whitelist. Meça antes de falar.
6. Retomada após queda de rede: validada pela primeira vez por uma falha real¶
01:52:40 tool_result: "Agent terminated early due to an API error" ← 一个 subagent 被打断
01:55:41 assistant: "API Error: ENOTFOUND" ← 主线程收到
↓ 探针挂着等网络恢复
resumed=True,续跑同一条会话,又跑了 8 个多小时到完成
O manifest registra attempts=2 / resumed=True / ok=True. 10 horas de trabalho não recomeçaram do zero. Antes disso, a resiliência ficava eternamente em "ainda não verificado" — cortar a rede exige sudo para mexer no hosts, e ninguém quer fazer isso durante uma run real.
Aquela mensagem sintética de "API Error" foi removida pelo PruningSessionStore no load, e o modelo não a viu — o design de prune também foi validado pela falha real.
Dois problemas que ficaram expostos (ver issue #2):
- A recuperação acontece na camada do framework, não na camada de transporte. O preço é reproduzir o contexto inteiro de novo, e aquele contexto era grande.
- O subagent que foi interrompido perdeu o trabalho pela metade. O resume salva a main thread; recuperação de interrupção no nível do subagent é outro problema.
7. A auto-verificação dele é real?¶
Este é o ponto que mais merece desconfiança: o agent escreve os próprios testes, roda sozinho e declara aprovação — é fácil virar teatro.
Depois do fato, foi despachado um subagent de auditoria independente, que leu estaticamente os 58 scripts e as 22 entregas. A conclusão é que é real, e com um rigor incomum:
- 0 asserções tautológicas. Em todo o repositório não existe padrão do tipo
[ 1 = 1 ]. - 8 ocorrências de
|| true, das quais 7 são legítimas (grep -cretorna 1 sem match, fallback deulimit, e coisas assim). - Julga comportamento, não existência de arquivo.
audit-idle-cost.sh:53deixa o IDE ocioso em um pty por 60 segundos, conta o número de requisições realmente recebidas pelo servidor falso local, e só dá PASS com-le 2.fix-p1-idle-verify.sh:100-110vai além: assere que o idle acontece exatamente 1 vez e assere que o ghost continua sendo exibido — evitando que "a mudança de rate limit mate a funcionalidade junto". - Sobe processos filhos de verdade.
wave5-pty-drive.cpp:316usaforkptypara rodar a TUI de verdade;audit-fake-ai-server.pyé um servidor de socket real, com cinco injeções de falha/err500 /err401 /badjson /drop /slow; todos os caminhos de AI forçam a limpeza deCPPIDE_API_KEY.
Duas coisas superaram as expectativas.
Ele fez controle positivo e negativo do próprio verificador. 验收审计.md:29-34: primeiro alimenta ZZZ_NEVER_APPEARS_ZZZ para confirmar que o driver dá FAIL, depois alimenta a string real para confirmar que dá PASS — evitando "auditar com uma máquina falsa que sempre dá PASS". Isso é verificar o próprio verificador.
Ele revelou espontaneamente suas asserções fracas. O for m in $must em wave5-b4-ai-states.sh:114 quebra "AI 错误 请求超时" por espaços em três substrings e faz grep de cada uma separadamente, mas AI e 错误 aparecem inevitavelmente em qualquer tela — nominalmente "3 verificações passaram", na prática só 1 tem poder discriminante. Isso foi ele mesmo que escreveu em 验收审计.md:576-582, não foi o auditor que descobriu.
Do mesmo tipo: wave5-verify.sh:296-302 usa 7 linhas de comentário para explicar por que o item B3b foi rebaixado para SKIP (asserção de timing da última tela, sem relação com o código de produto), anotando que "a lógica de julgamento e a faixa de tolerância não tiveram uma palavra alterada"; :236-238 registra que ele descobriu e corrigiu uma asserção falsa que ele mesmo tinha escrito antes — o if pgrep ... cai silenciosamente no else em uma imagem enxuta sem pgrep, virando um PASS eterno.
A evidência qualitativa de reuso bate com os números da seção 4: audit-fake-ai-server.py é reusado por 7 scripts; wave5-pty-drive.cpp é reusado por 5+ compilações, com o cache de compilação feito por [ -x "$DRIVE" ] ||; o driver de wave4 → wave5 é uma extensão, não uma reescrita (o diff mudou só 1 linha e adicionou 343, com as 281 linhas do wave4 preservadas intactas).
Todos esses comportamentos aconteceram em uma versão sem goal guard. A única restrição eram os 10 critérios de aceitação julgáveis escritos no brief. Isso mostra: escrever o "pronto" de forma julgável já é suficiente para induzir auto-auditoria e auto-revelação. O valor marginal do goal guard tem que ser medido tomando isso como linha de base, não zero.
Mas a próxima seção mostra: a auto-auditoria é muito forte na parte técnica de verificação, mas errou o veredito justamente no item mais importante.
O "auditor" aqui não é o judge
O auditor independente das seções 7 e 8 é um subagent despachado espontaneamente pelo coordenador, não o judge do goal guard — esta run simplesmente não tem goal guard. Os dois parecem iguais na leitura, mas fazem coisas diferentes: o auditor é uma retrospectiva pós-fato, o judge é o portão de verdict no fim de cada turno.
8. A nota que ele deu a si mesmo está certa? — o critério 1 foi julgado errado¶
Foi despachado um segundo auditor independente para julgar aqueles 10 critérios de aceitação, um a um, contra o código.
Os critérios 2–10 são todos verdadeiros, com evidência no código. O modo treino não injetar código é até uma garantia estrutural: único ponto de decisão de sink em ai.h:117 + único switch de aterrissagem em app.cpp:590-615 + quatro early-returns em editor.cpp:679-683, não ifs espalhados. Os P1–P4 que ele mesmo encontrou também são todos verdadeiros, com todas as correções aplicadas (P4 é meio-verdadeiro: aquela linha do pair azul é código morto, ghostAttr() já passava por A_DIM).
Mas o critério 1 foi julgado errado.
标准原文: "编译出一个独立可执行文件,在 macOS 终端直接运行"
交付的产物: ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux
它的判定: 通过
Este item foi verificado à mão por uma pessoa rodando file.
Ele não escondeu nada — o título da §4 de 交付说明.md é literalmente "o maior risco não verificado: nunca rodou no macOS nenhuma vez", e a §6 da auditoria lista seis frentes não verificadas do macOS. Ele revelou o risco, mas nunca reconectou esse fato ao veredito do critério 1, sendo que a exigência literal do critério 1 é exatamente esse ponto.
Mas isso não é evidência de que "é preciso um judge independente"¶
É fácil ler isso de bate-pronto como "logo, precisamos do goal guard". Errado, e ler assim é atirar a flecha e desenhar o alvo depois — exatamente o tipo de raciocínio motivado que a seção anterior acabou de criticar.
A versão do goal guard daquela época, na configuração padrão, muito provavelmente também teria deixado esse item passar. O judge() usa can_run=False por padrão, com apenas as ferramentas Read / Glob / Grep — ele não consegue rodar file. Ele iria ler o código-fonte, ver que Makefile:25-38 de fato tem um branch Darwin, e julgar "atingido". Exatamente a mesma falha da auto-auditoria.
O fato estrutural de verdade¶
O critério 1 é simplesmente inverificável no ambiente desta run.
Ele assere "o comportamento do artefato no macOS", e a run inteira aconteceu em um container Linux. Nenhum judge consegue verificar um binário macOS dentro do container, seja ele auto-auditoria ou independente. O problema não é "quem julga", é que esse item foi tratado como um critério julgável comum, e então foi julgado.
Outros três achados do auditor (nenhum mencionado pela auto-auditoria)¶
- A semântica de cota do
doTab():app.cpp:909chamanoteActivity()incondicionalmente depois deacceptGhost()→gen_++→ reabre a cota, de modo que cada Tab aceito dispara mais uma requisição automática 500ms depois. A análise do P1 (queimar dinheiro em idle) só auditou os três pontos de cancelamentosetMode/cancelInFlight/askNow, e deixou passar esse caminho de gasto, que é o mais frequente. - O "cinza" do critério 6 só é literalmente verdadeiro em terminais de 256 cores; no modo 8 cores está fixado em
A_DIM(escurecido, não cinza). - O subtítulo da §7 de
验收审计.mddiz "9 critérios de aceitação", mas a tabela tem 10 linhas — erro de digitação, mas aparece no cabeçalho da avaliação geral.
9. Escala final de spill e workbench¶
103 spills, 791.4K caracteres convertidos em ponteiros de caminho, sem residir no contexto.
.flower/ no final: 58 scripts (250K), 22 entregas (330K), 5 notas (43K).
Risco remanescente¶
O alvo de entrega é macOS, mas toda a verificação foi feita dentro de um container Debian aarch64, com zero verificação em máquina macOS real. Esse é o efeito colateral real da solução de isolamento por container: para ter segurança, ele foi trancado em Linux, mas o que ele precisa entregar é macOS. O próprio README do projeto, na §10.24, registra isso honestamente e lista 4 pontos de risco, mas não fecha o ciclo.
Essa lacuna foi coberta depois pelo HT002 — aquela run foi justamente instalar esse binário em um macOS de verdade.
Um benefício inesperado: dentro do container só se enxerga /work, então nos 19MB de transcript o nome de usuário do host /Users/hechenyu vaza zero vezes (/work aparece 11,495 vezes). O isolamento isolou os caminhos de quebra.
O que esta run não conseguiu verificar¶
- Goal guard: esta run é anterior à sua introdução. O coordenador organizou espontaneamente uma auditoria independente, tecnicamente muito sólida na verificação, mas errou o veredito no item mais importante (seção 8). As duas conclusões precisam ser registradas juntas — escrever critérios julgáveis no brief já induz a auto-auditoria, então a linha de base do goal guard não é zero; e o goal guard na configuração padrão daquela época também teria deixado esse item passar, então ele também não é bala de prata.
- Comportamento da janela de 1M: só 18.6% foi usado, sem tocar no limite.
- Comportamento do micro-compact sob
DISABLE_AUTO_COMPACT=1: nenhum compact foi disparado durante a run, então segue sendo inferência.
O que esta run mudou no framework¶
| Achado | Mudança aplicada |
|---|---|
which barrado (seção 5) | is_ephemeral() ganhou which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers; tests/glance.py ganhou 7 casos adversariais guardando a forma de consulta de command / type |
| Critério 1 julgado errado (seção 8) | O item 1 de JUDGE_RULES passou a julgar o artefato, não o código-fonte — se der para rodar, rode; se não der, inspecione o próprio artefato (plataforma, tamanho, se carrega) |
| "Eu revelei o risco" rendeu uma aprovação | O verdict tem que distinguir "não foi feito" de "aqui não dá para verificar". Verdict.parse agora reconhece 无法验证 / 没法验证 / 无法判定 como um terceiro estado unreachable → sobe para perguntar a uma pessoa, nunca pode ser aprovado |
| O critério 1 é, por princípio, inverificável neste ambiente | O passo de definição do objetivo passa a exigir que os itens inverificáveis neste ambiente sejam marcados na hora, em vez de descobrir isso depois de terminar o trabalho |
| Recuperação de queda de rede na camada do framework, trabalho parcial do subagent perdido (seção 6) | Registrado como issue #2, ciclo não fechado |
Os três primeiros itens foram para a API real pela primeira vez no HT002, e ali o "julgar o artefato, não o código-fonte" salvou uma situação na hora — mas a mesma regra também produziu uma nova falha.