Pular para conteúdo

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:

python tools/analyze_run.py /path/to/cppide/runs

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:

第 1轮   28.7K   ← 启动地板(此前实测约 34k,对上了)
第20轮   35.2K
第35轮  108.6K
第50轮  158.2K
第70轮  185.9K   ← 结束
  • 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

总输入 299.4M token
  命中缓存  287.5M  (96.1%)
  新建缓存   11.8M
  未缓存      6.2K
输出 2.3M(其中 thinking 653.9K)

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:

61 个脚本:被写 95 次、被执行 331 次
执行次数 > 1 的(真被复用):56 个,占 92%
写了但一次都没执行的:0 个
执行/写入比 3.48

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):

  1. 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.
  2. 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 -c retorna 1 sem match, fallback de ulimit, e coisas assim).
  • Julga comportamento, não existência de arquivo. audit-idle-cost.sh:53 deixa 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-110 vai 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:316 usa forkpty para 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 de CPPIDE_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 / Grepele 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:909 chama noteActivity() incondicionalmente depois de acceptGhost()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 cancelamento setMode / 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 验收审计.md diz "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 unreachablesobe 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.