HT002 · Instalar no macOS e fazer rodar¶
2026-09-07, a tarefa cabe em uma frase: clonar o cppide, instalar e fazer rodar nesta máquina macOS. O resultado foi $38.24 / 4 steps / cerca de 1 hora, o programa de fato rodou no macOS (PID 96040, Mach-O arm64), e o veredito foi inalcançável, subiu para perguntar à pessoa, que escolheu aceitar.
Esta foi a primeira run com goal guard —— tudo o que saiu das lições de HT001, naquele mesmo dia, foi pela primeira vez para a API real. Ela salvou um buraco em que HT001 caiu e, ao mesmo tempo, produziu um novo fracasso em direção exatamente oposta: transformou git clone && make && ./cppide em uma hora. A parte mais valiosa desta página é a segunda.
O registro bruto está em human-test/HT002/, e a contabilidade é calculada a partir de runs/manifest.json e runs/sessions.db:
O formato desta run¶
| Item | Valor |
|---|---|
| Tarefa | clonar o cppide, instalar e fazer rodar nesta máquina macOS, com a pessoa conseguindo abrir a interface e usar |
| Custo | $38.24 —— 4 steps, detalhamento abaixo |
| Duração | cerca de 1 hora (total de 0.97h) |
| Turnos | 70 (soma dos num_turns dos quatro steps) |
| Contexto da main thread | 36.8K → 80.2K, cai de penhasco nas fronteiras de step, não cresce monotonicamente |
| Divisão de trabalho | 6 subagents; coordinator com 7 chamadas de ferramenta vs 212 dos subagents |
| Cache | acerto de 90.8% |
| Workbench | 5 artefatos, 4 notas, 11 arquivos de spill, 0 scripts reutilizáveis |
| Resultado | rodou (PID 96040, Mach-O arm64); veredito inalcançável, a pessoa escolheu aceitar |
| Versão | primeira run com goal guard |
Custo, turnos e duração de cada step¶
| Step | Custo | Turnos (num_turns) | Duração |
|---|---|---|---|
| Confirmar requisitos | $0.5306 | 9 | 0.10h |
| Definir objetivo | $0.4117 | 12 | 0.05h |
| Trabalhar | $35.8950 | 12 | 0.73h |
| Trabalhar · veredito #1 | $1.4037 | 37 | 0.09h |
| Total | $38.2409 | 70 | 0.97h |
A coluna de duração é a soma dos valores já arredondados de cada step. Recalculando pelo registro bruto, a soma exata dos quatro duration_s no manifest.json é 3471.53 segundos = 0.9643h; esta página mantém 0.97h, uma diferença de 0.006h.
O dinheiro está quase todo no step "trabalhar" —— $35.8950 são 93.9%. A primeira seção abaixo explica onde esse dinheiro foi parar, e por que ele não deveria ter sido gasto assim.
1. Manchete: transformou git clone && make && ./cppide em uma hora¶
Esta é a descoberta mais importante desta vez, mais importante que todos os "mecanismos funcionaram" abaixo.
No step de definir objetivo, o judge escreveu para si mesmo 15 itens de checklist de veredito (o texto original está em 目标.md, sob .flower/notes/). Classificados por natureza:
| Categoria | Itens | Exemplo |
|---|---|---|
| Verificam de verdade "a coisa funciona" | cerca de 5 | build exit 0; processo vivo ≥30 segundos; comando de reinício reexecutável em um novo shell |
| Verificam "o processo seguiu as regras" | 6 | mtime de ~/.zshrc / .zprofile / .bash_profile anterior a esta sessão; não rodou brew install / npm -g / sudo; mtime de .flower/ inalterado, runs/ sem adições ou remoções manuais |
| Impossíveis de verificar por princípio | 4 | tirar screenshot e olhar a imagem; console do devtools; clique manual; confirmação da própria pessoa |
Apenas um terço verifica "se essa coisa funciona ou não". Seis itens verificam se ele mesmo seguiu as regras —— e um deles checa o mtime do diretório .flower/, que é o diretório do próprio framework.
Aquele item que checa .flower/ reprovou a si mesmo¶
Isso não é um problema teórico. Na rodada de veredito, o judge escreveu com honestidade em "não aprovados":
mtime de todos os arquivos sob
.flower/anterior ao início da sessão —— falha literal:INDEX.md16:14,artifacts/01–05(15:40–16:12),notes/决策-项目形态与验收路径.md15:43, todos escritos dentro da sessão.
O motivo: o framework exige rigidamente que artefatos longos sejam escritos em .flower/artifacts/ e decisões em .flower/notes/. Aquele item do checklist exige que esse diretório não seja tocado. Duas regras colidem diretamente, e a colisão foi fabricada pelo próprio checklist de veredito —— o worker não escreveu nem um artefato longo, e isso o deixaria mais conforme.
Ele não passou por cima disso: o step de trabalhar listou esse conflito por conta própria no relatório final, e o registro de decisão também documentou o tratamento —— nenhum arquivo existente foi tocado (需求.md, 目标.md, 问答记录.md, os três com mtime e MD5 idênticos antes e depois), só houve adições. O judge, após revisar, concordou que "a adulteração que este item pretende evitar não aconteceu", mas ainda assim, seguindo a regra, o listou como não aprovado e deixou a decisão para a pessoa.
Causa raiz 1: fronteiras foram tratadas como itens de veredito¶
As fronteiras levantadas na fase de clarify ("só pode instalar dentro do diretório do projeto", "não mexer no código de negócio") deveriam restringir como trabalhar, mas acabaram escritas como itens que verificam o artefato. Daí surgiram entradas do tipo checar o mtime de .zshrc, checar o mtime de .flower/.
Fronteira ≠ item de veredito. Fronteira restringe o processo, item de veredito verifica o resultado. Uma vez misturados, cada fronteira adicionada equivale a um veredito adicional —— e fronteiras são justamente o que se incentiva encher na fase de clarify —— CLARIFIER_RULES diz explicitamente "deixe claro o que não fazer. Este trecho vai governar cada pessoa que trabalhar depois".
Comparando com o registro bruto dá para ver cada elo dessa cadeia de amplificação: a fronteira do brief dizia "não mexer no .flower/ e no runs/ já existentes no diretório atual", e no checklist de veredito isso escalou para "mtime de todos os arquivos sob .flower/ anterior ao início desta sessão" —— de "não mexer no que já existe" para "o diretório inteiro não pode ter arquivos novos".
Causa raiz 2: JUDGE_RULES tem pressão em uma única direção —— e isso foi adicionado naquele mesmo dia¶
Mais cedo naquele dia, a partir das lições de HT001, foram escritas estas coisas no JUDGE_RULES:
- "cada item precisa ser verificável na hora", "o que faltar, o que for vago, você completa"
- "julga-se o artefato, não o código-fonte" (com aquele contraexemplo do macOS)
- "itens que não podem ser verificados no ambiente atual, marque na hora"
- "primeiro veja bem onde você está (pode usar
uname -a)"
Cada uma dessas empurra para mais checagem, mais rigor. Em toda a regra não há uma única frase dizendo "seja proporcional ao tamanho da tarefa".
Ou seja: otimizar contra o fracasso de "declarar aprovação em excesso" de HT001 produziu exatamente o fracasso oposto —— checagem em excesso. Isso foi fabricado por nós, não é problema do modelo.
Causa raiz 3: somou-se com a issue #3¶
O que realmente travou foram dois defeitos de portabilidade no macOS:
src/proc.cppusa::sigemptyset, e o SDK da Apple, depois do protótipo da função, ainda a define como macro de função (guardada apenas por#ifndef _ANSI_SOURCE), então::sigemptyset(&s)expande para::(*(&s)=0,0), erro de sintaxesrc/ui.cppusa::_exitsem incluir<unistd.h>
Três linhas resolveriam. Mas o usuário havia respondido na fase de clarify "não mexer no código de negócio", e o agent executou estritamente, então deu a volta tentando flags de compilação, despachando subagents, escrevendo registros de decisão. Se ele pudesse ter dito no meio do caminho "pode mexer naquelas duas linhas, sem problema", teria acabado em dez minutos. Uma fronteira escrita como proibição será interpretada com o máximo rigor, e no meio do caminho não existe nenhum canal para afrouxá-la.
O registro bruto mostra o tamanho desse desvio: foram quatro pontos de bloqueio no total, e apenas o do ::_exit podia ser resolvido só com flags de compilação; o do ::sigemptyset teve 6 combinações de -D/-U testadas, todas falharam (a família _POSIX_C_SOURCE ainda quebra de quebra st_mtimespec e O_CLOEXEC). Esse beco sem saída, por si só, gerou um relatório de viabilidade de 13.6K.
Mas sendo justo: a fronteira forçou uma solução melhor¶
A solução final não foi alterar o código-fonte, e sim gerar no Makefile um header shim que só faz #undef e forçá-lo à frente com -include:
$(MACSHIM):
@printf '#include <signal.h>\n#undef sigemptyset\n#undef sigfillset\n...' > $@
CPPFLAGS += -include $(MACSHIM) -include unistd.h
Isso é melhor que alterar o código-fonte —— o repositório upstream não muda uma linha, e qualquer um que clone consegue compilar. O shim é gerado em build/, não entra no repositório, o clean ganhou rm -rf build para recolhê-lo, e make -n UNAME_S=Linux mostra na prática que a expansão não tem -include, o caminho Linux não foi contaminado. No fim, o git diff tem um único arquivo, Makefile, +14 / −2.
Ou seja, a fronteira produziu uma solução melhor a um custo maior. O problema não é que ele fez algo errado, é que esse trade-off não foi levado a uma pessoa —— e o motivo de não poder ser levado é exatamente a issue #3.
Por que o git status tem 64 entradas e ainda assim se diz zero alteração no código-fonte
O repositório do cppide commitou 30 .o, 30 .d e 2 binários no controle de versão, e são ELF Linux aarch64, com mtime posterior aos .cpp —— sem um make clean antes, o make pula a compilação e vai direto linkar os ELF, e quebra na certa. Então, das 64 entradas de git status --porcelain, 63 são artefatos de compilação rastreados pelo git mais um ?? build/. Filtrando por extensão, git diff -- 'src/*.cpp' 'src/*.h' README.md config.sample.json | wc -c dá 0. Essa formulação precisa foi forçada pela auditoria de conformidade —— o coordinator originalmente queria escrever de forma genérica "zero alteração em src/", e a auditoria apontou que a frase não se sustentava.
2. O que o goal guard fez certo¶
Do lado dos mecanismos, tudo funcionou, e as duas alterações daquele dia deixaram impressão digital direta nos artefatos.
O primeiro item do checklist de veredito que ele escreveu:
uname -amostra Darwin 25.2.0 (macOS)…… todos os itens de "rodar/abrir" abaixo precisam ser resultados obtidos rodando de fato nesta máquina, não se aceita inferência do tipo "o código-fonte tem um branch de macOS, então deveria rodar"
Essa é exatamente a frase escrita no JUDGE_RULES naquela manhã a partir das lições de HT001.
Ele usou três vezes a marcação [此环境无法验证:…] adicionada naquele dia, cada uma com sua justificativa —— console do devtools (não há navegador acionável, e instalar um headless para isso ultrapassaria a fronteira), clique manual (precisa de uma pessoa diante da tela), confirmação da própria pessoa (só ela pode dar).
A primeira frase do judge foi "eu não tiro conclusão a partir daquela resposta. Vou ao local." E então:
file cppide → Mach-O 64-bit executable arm64
lsof -p 96040 → txt aponta para /Users/hechenyu/explore/test-ide/cppide/cppide
iniciado às 16:10, ainda vivo às 16:15
Ele mesmo escreveu: "aquele buraco de 'entregar um ELF Linux' das lições do meta, desta vez não caímos nele. Um ELF Linux não tem como virar um PID no Darwin. Isso não foi inferido do Makefile."
Ele também conferiu que o código-fonte realmente não foi tocado: o mtime de todos os .cpp / .h sob src/ é 15:32 (momento do clone), o que mudou foi só o Makefile (16:06), e os .o / .d (16:07) são artefatos de compilação rastreados que foram reconstruídos.
Ele também declarou honestamente onde não alcançava. O Bash do judge estava restrito ao diretório de trabalho, então a comparação com git ls-remote e o mtime de ~/.zshrc ele não conseguiu rodar, tendo apenas de confiar na saída da auditoria independente —— esses dois itens ele listou separadamente explicando "não consegui reexecutar com as minhas próprias mãos".
3. Veredito "inalcançável", e então pergunta à pessoa¶
O veredito de três estados mais a intervenção humana: a cadeia inteira rodou de verdade pela primeira vez.
A conclusão do veredito foi inalcançável, travada naquele item do screenshot: tanto screencapture -x quanto -l <window-id> retornaram could not create image from display —— a permissão TCC de gravação de tela não foi concedida, e conceder exige que uma pessoa vá clicar nas configurações do sistema, o que por si só é uma alteração de sistema proibida pela fronteira.
"Despachar mais uma rodada de subagents não vai fazer surgir essa imagem. Pela regra, isso não pode ser aprovado, e também não deve ser julgado 'não atingido', é simplesmente inalcançável, é hora de parar e entregar para a pessoa."
Com isso, os itens "impossíveis de verificar por princípio" do checklist passaram de 3 para 4 —— 4 dos 15 itens simplesmente não podem ser verificados neste ambiente, e isso só foi descoberto depois de gastar $35.8950 em trabalho e $1.4037 em veredito. O step de definir objetivo custou apenas $0.4117; era ali que isso deveria ter sido descoberto.
O framework levou essa conclusão à pessoa (registrado no registro de perguntas e respostas como q7), com três opções; a pessoa escolheu "aceitar este resultado", o gate liberou, e o workflow foi concluído.
Ele também recusou um substituto. O worker usou AppleScript para ler de volta o texto da janela do terminal, que era a TUI realmente renderizada —— coluna de números de linha, corpo de src/main.cpp, barra de painéis ─[编译]─[运行]─[AI*]─[输入]─, barra de status 练习模式 │ main.cpp │ 1:1 │ C++, não era tela branca nem tela de erro. O judge disse:
"Quem trabalhou não tentou passar isso por 'olhar a imagem', essa autocontenção está certa. Mas isso não substitui o item exigido pelo checklist, e eu também não vou contar por ele."
"Divulgar o risco" não equivale a aprovar
Foi exatamente nisso que HT001 caiu: escreveu "nunca foi rodado no macOS" na nota de entrega, e ainda assim aprovou o critério 1. Em HT002, o judge tinha em mãos uma evidência substituta melhor (texto real da TUI) e ainda assim recusou usá-la no lugar do item do checklist. Essa diferença é todo o sentido do design de três estados do veredito —— UNREACHABLE não é um eufemismo para NOT_YET, é "hora de parar e perguntar à pessoa".
4. allowed_tools não é uma whitelist rígida¶
Ao analisar esta run, descobriu-se um fato que contradiz o que afirmamos. Separando as chamadas de ferramenta por sessão:
| Sessão | O que de fato usou | O que há na whitelist |
|---|---|---|
| Confirmar requisitos | WebFetch, Glob, ask×6 | clarify() na época só dava ask + Read/Glob/Grep |
| Definir objetivo | Bash ×11 | judge() tem can_run=False por padrão, sem Bash |
| Trabalhar · veredito | Bash ×31, WebFetch | idem acima |
| Trabalhar (coordinator) | Bash ×1, Agent ×6 | correto |
goal_step não tem nenhum caminho de código que passe can_run, então a linha "definir objetivo" não pode ser problema de configuração.
Verificação com uma sonda de $0.1: dar a um agent com allowed_tools=["Read"] a tarefa de escrever um arquivo ——
Write → camada de permissão: "requested permissions to write ... but you haven't granted it yet"
Bash → segurança de caminho: "Output redirection was blocked. For security, Claude Code may
only write to files in the allowed working directories"
O modelo consegue chamar ferramentas que não estão na whitelist, apenas é barrado pela camada de permissão e pela segurança de caminho. Portanto:
allowed_toolsé uma lista de dispensa de aprovação, não uma whitelist exclusiva- o que de fato protegia
clarify()/judge()na época era opermission_mode="default"herdado - e ambos tinham
delegate_only=False→ sem aquele hookdelegate_guard. O coordinator tinha o hook protegendo; esses dois papéis, naquele momento, não tinham nenhum mecanismo próprio do flower
A docstring de clarify() diz "sem ferramentas de escrita —— ele não tem como começar a trabalhar", e todo aquele contraexemplo medido a $0.8908 foi feito para essa frase. A justificativa mecânica dessa frase está errada.
Os dois fatos desta seção mudaram depois
O whitelist_guard já foi ligado incondicionalmente: papéis com delegate_only=False agora recebem esse hook automaticamente do Runtime, e clarify() / judge() não estão mais "sem nenhum mecanismo". Além disso, WebFetch agora está na whitelist de clarify(), então aquele exemplo da primeira linha da tabela ficou inválido —— a conclusão continua de pé, mas agora se apoia apenas na evidência da sonda de $0.1. Veja a última seção desta página.
5. Curva de contexto: fronteiras de step resetam¶
turno 1 36.8K
turno 25 80.2K ← pico
turno 26 32.9K ← penhasco: novo step = nova sessão (resume_from=None)
turno 121 64.8K
Três dos quatro pontos amostrados (turnos 1 / 25 / 121, 36.8K / 80.2K / 64.8K) batem um a um com o registro bruto; o 32.9K do turno 26 não bate no recálculo: reproduzindo as mensagens assistant da main thread ordenadas por store_key, a 26ª (a primeira da nova sessão) tem input + cache_read + cache_creation de 31,972 tokens = 32.0K, enquanto 32.9K é o segundo valor da mesma sessão (31,970 + 948). Esta página mantém 32.9K —— a diferença é de 0.9K, e a conclusão de "queda de penhasco" se sustenta com qualquer um dos dois valores.
HT001 subiu monotonicamente até 185.9K —— um único step rodou por 10 horas. HT002, por estar dividido em quatro steps, abre uma nova sessão a cada step, e o contexto é resetado estruturalmente. Esse é o efeito direto daquele design de "brief e objetivo são artefatos congelados, e os passos seguintes recebem os documentos, não a conversa" —— dá para ver na curva.
Acerto de cache de 90.8% (HT001 foi 96.1%). A taxa de acerto sobe com o comprimento da sessão, então sessões curtas saem um pouco mais caras por unidade —— isso é a outra face da mesma moeda da conclusão da terceira seção de HT001.
6. Comparação com HT001¶
| HT001 | HT002 | |
|---|---|---|
| Tarefa | escrever um IDE de terminal do zero | instalá-lo no macOS e fazer rodar |
| Custo / duração | $171.62 / 10.44h | $38.24 / 0.97h |
| Steps | 2 (sem goal guard) | 4 (com definir objetivo + veredito) |
| subagents | 23 | 6 |
| Chamadas de ferramenta do coordinator | 32 | 7 (das quais 6 são Agent) |
| Chamadas de ferramenta dos subagents | 1,893 | 212 |
| Contexto da main thread | 28.7K → 185.9K subindo monotonicamente | 36.8K → 80.2K, reset nas fronteiras de step |
| Acerto de cache | 96.1% | 90.8% |
| Scripts do workbench | 58, executados 331 vezes, 92% reutilizados | 0 |
| Veredito | nenhum (auditoria interna espontânea do coordinator) | veredito de três estados, julgou "inalcançável", perguntou à pessoa |
A última linha merece atenção. HT002 não escreveu nenhum script reutilizável —— a tarefa durou apenas uma hora, não havia nada que valesse sedimentar. Isso mostra que o valor do workbench cresce com o comprimento da tarefa; em tarefas curtas ele é puro overhead.
Como é o registro bruto¶
.flower/INDEX.md é o snapshot do workbench no fim desta run, dá para olhar diretamente:
| Diretório | Conteúdo |
|---|---|
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 são três artefatos congelados escritos pelo framework; o registro de decisão escrito pelo próprio agent é só 1 |
spill/ | 11 arquivos de spill |
Sob runs/ estão o manifest.json (4 registros de step) e o sessions.db. No banco há 10 sessões: 4 sessões de main thread mais 6 sessões de subagent —— bate com os "6 subagents".
No registro de perguntas e respostas há 7 perguntas no total: q1–q6 foram feitas na fase de clarify (batem com as 6 chamadas de ask do step de confirmar requisitos), e q7 é aquele portão humano depois do veredito "inalcançável".
O que esta run mudou no framework¶
| Descoberta | Mudança implementada |
|---|---|
| Dos 15 itens do checklist de veredito, apenas cerca de 5 verificam se a coisa funciona (seção 1) | JUDGE_RULES ganhou pressão em sentido contrário: o comprimento do checklist é determinado pelo número de modos de falha, não pelo grau de rigor, e esta run virou contraexemplo escrito no texto da própria regra ("uma tarefa de 'instalar e rodar' foi escrita como 15 itens, e só 5 verificam se funciona" —— na regra o número é arredondado para 5; o critério de classificação da seção 1 desta página é cerca de 5) |
| Fronteiras foram escritas como itens de veredito | JUDGE_RULES fixa a frase "fronteira não é item de veredito" |
| 4/15 itens não podem ser verificados neste ambiente, e isso só foi descoberto depois de gastar $35.8950 + $1.4037 | O step de definir objetivo agora exige acrescentar na hora o sufixo [此环境无法验证:原因] aos itens não verificáveis —— desta vez ele acertou 3, e deixou passar o do screenshot |
clarify() / judge() sem proteção de hook (seção 4) | whitelist_guard já ligado incondicionalmente: papéis com delegate_only=False recebem o hook automaticamente do Runtime, sem depender de o workbench estar ligado |
| Efeito colateral: o judge de definir objetivo agora não tem Bash | Depois do whitelist_guard, sem passar can_run=True explicitamente, aquele item do JUDGE_RULES de "primeiro use uname -a para ver bem onde você está" não pode ser executado —— esse é um trade-off que surgiu depois da correção |
A justificativa mecânica da docstring de clarify() estava errada | Corrigida. WebFetch também entrou na whitelist de clarify(), então aquele exemplo ficou inválido, e a conclusão passou a se apoiar na sonda de $0.1 |
| Não há canal para dizer uma frase no meio do caminho (causa raiz 3 da seção 1) | Registrado como issue #3, não fechado —— desta vez ele se somou diretamente à complicação excessiva; um "pode mexer naquelas duas linhas, sem problema" teria economizado uma hora |
Resumindo esta run em uma frase: otimizar contra o fracasso da vez anterior produz muito facilmente um novo fracasso na direção oposta. HT001 ensinou ao framework "não aprove com facilidade"; HT002 imediatamente demonstrou o que acontece quando essa regra não vem acompanhada de "não cheque demais". As duas precisam estar no JUDGE_RULES; faltando uma, ele pende.