Agentes escrevem código mais rápido do que qualquer um consegue revisar. Eis o ratchet que impede a base de código de apodrecer

Você pede a um agente que refatore um módulo. Ele devolve um diff de 800 linhas, você dá uma olhada por cima, você mescla. Três meses depois você abre aquele arquivo: 4.000 linhas, uma dúzia de ramificações por função e uma hora de leitura antes de conseguir mudar uma linha com segurança. O problema não é o agente escrever código ruim — é que a velocidade de geração ultrapassou a velocidade de revisão. Tem coisa capaz de adicionar 5.000 linhas por dia; ninguém consegue ler 5.000 linhas por dia. Bases de código raramente são arruinadas por um único commit terrível; elas são esmagadas por mil commits que cada um adicionou “só um pouquinho”.
Em 5 de outubro de 2026, o repositório do Hermes mesclou o PR #132646, que instala um mecanismo interessante: a saúde do código só pode melhorar. Ele não persegue um ideal. Funciona como um ratchet — cada função e cada arquivo mantém o próprio nível atual e só pode se mover numa direção. Veja como funciona e o que vale a pena copiar.
A regra central: tetos separados, sem empréstimo
A regra é curta e dura:
- O que já está acima da meta não pode crescer. Uma função com complexidade ciclomática (CC) 40 — uma contagem aproximada dos caminhos independentes pelo código — pode ficar em 40 ou cair para 20, mas nunca chegar a 41.
- Código novo tem que atingir a meta. Funções e arquivos novos precisam satisfazer: CC 20, 300 linhas próprias por função, aninhamento 6, 2.000 linhas por arquivo.
A metade mais sutil é que você não pode pegar orçamento emprestado. Violações de regra de padrão (digamos, uma exceção engolida) são rastreadas por fingerprint — a regra, a função que a contém e o texto da linha. Corrigir uma violação nunca paga por adicionar outra, e nada é somado entre arquivos. Sem isso, um ratchet vira um placar para manipular.
Sem arquivo de baseline — a parte esperta
A maioria das ferramentas de saúde de código quer um baseline: registre toda violação existente, proíba ultrapassá-la. Esse arquivo vira um ímã de conflitos, porque dois PRs paralelos querem editá-lo e quem mescla primeiro vence.
O Hermes não guarda baseline nenhum. Ele mede na hora: pega os arquivos que uma mudança toca, tanto na base (o primeiro pai do merge commit) quanto no head, e compara. Três consequências:
- uma melhoria que cai na
mainvira o novo teto na hora, sem ninguém precisar lembrar de “apertar o ratchet”; - PRs paralelos nunca conflitam por causa do estado do ratchet;
- os dois lados são sempre medidos com a mesma versão da ferramenta, então a comparação faz sentido.
Código movido mantém o teto; código copiado não
É isso que mantém a refatoração viável:
- Funções são casadas por um hash do corpo que ignora o nome. Uma função movida para outro arquivo e levemente editada (similaridade de corpo ≥ 0,6, uma saiu e uma chegou) mantém o teto, e instruções de nível de módulo movidas literalmente mantêm o delas.
- Uma cópia que deixa o original no lugar conta como código novo, e tem que atingir a meta.
- Arquivos renomeados seguem a detecção de rename do git, em vez de serem tratados como novos.
- Funções aninhadas são unidades próprias, então editar uma closure não é debitado à função que a contém. A complexidade de Python usa o C901 do ruff menos as defs aninhadas diretas.
O que mais ele bloqueia
Tamanho é uma categoria. O conjunto completo de regras (com o escopo e o texto de correção que o agente vê) fica em scripts/code_health/config.py:
| Categoria | Regras |
|---|---|
| Exceções engolidas | S110, BLE001, E722 (except: puro); um logger.warning("...: %s", e) só com mensagem ainda conta como engolida |
| Correção | F821/F823/F811/F841, RUF006 |
| Pode travar | ASYNC230/ASYNC240, S113, HX006 (subprocess/urlopen sem timeout), HX007 (I/O de config síncrono dentro de async def), HX008 (get_event_loop), HX009 (resultados de gather checados contra Exception) |
| Escopo de profile | HX001 (~/.hermes hardcoded), HX002 (uma nova env var HERMES_*), HX004 (UnscopedSecretError caindo para os.getenv), HX005 (captura em tempo de import), HX012 (threading.Thread cru), mais PS-P05/PS-P06 |
| Outros | HX003 (identidade de processo via argv), HX010 (bool() de uma string de env), HX011 (escadas longas de if/elif) |
Um subconjunto é apenas de aviso: HX001, HX003, HX010, HX012, PS-P05, PS-P06. Amostras dos revisores põem a precisão delas em torno de 12–60%, então elas imprimem em toda execução mas nunca bloqueiam um commit. Transformar uma em bloqueante é uma mudança de uma linha, quando houver evidência de replay.
A saída de escape: dispense a regra, mas diga por quê
Toda regra precisa de exceções. A sintaxe é:
# health: allow BLE001 -- <why>
O motivo é obrigatório, e toda dispensa usada numa execução é listada na saída, então o revisor vê quantas dispensas uma mudança adiciona e o que elas dizem. É uma melhoria real em relação ao # noqa, que normalmente é silencioso.
Um comando, exatamente o que o CI roda
A parte mais prática é a experiência local. python scripts/check roda o ruff, as verificações de guarda (footgun / shebang / no-tmp / config-writer / OS-marker / case-collision / lazy-deps / profile-archive) e o próprio ratchet em paralelo, usando o ruff fixado do CI — tirado do grupo dev do pyproject, caindo para uvx ruff@<pin> em vez de usar silenciosamente o ruff que você tiver. Uns dez segundos na árvore.
python scripts/check # tudo, contra o merge-base com origin/main
python scripts/check --only health # apenas o ratchet de saúde de código
python scripts/check --staged # julga o index (o que o hook de pre-commit roda)
python scripts/check --commit REV # julga um único commit (o que o hook de pre-push roda)
python scripts/check --install-hook pre-push
python scripts/check --uninstall-hook
Os hooks são opt-in e podem ser pulados com --no-verify; o CI não. O scripts/check precisa de Python 3.11+ (para o tomllib) e diz isso de forma clara, apontando para a virtualenv do repositório, em vez de morrer num traceback a cada commit com hook.
Há também um interruptor geral: ENFORCEMENT = "blocking" | "advisory" | "off" em scripts/code_health/config.py. Ponto crucial: ele é sempre lido da main — no CI, do primeiro pai do merge commit; localmente, do origin/main — nunca do branch em teste. Um commit de uma linha na main reduz o ratchet para todo PR aberto, e nenhum PR consegue afrouxar a própria checagem.
Foi validado num replay congelado antes de bloquear qualquer coisa
O pior modo de falha de um novo portão é bloquear coisas que na verdade estão boas, então ele foi reproduzido contra o histórico primeiro: todo PR mesclado entre 2026-10-02 e 2026-10-03.
| Verificação | Resultado |
|---|---|
| Replay congelado (318 PRs) | 59 seriam bloqueados. 38 só por crescimento de arquivo; 21 por outras regras: crescimento de CC ou uma função nova acima de CC 20 em 10 PRs, 13 excepts cegos em 9 PRs, cinco F841 num arquivo de teste, um F811 de teste (um teste sombreado que nunca rodou), um S110, uma função nova de 300+ linhas |
| Ratchet de tamanho de arquivo | O único bloqueador de 38 dos 318 PRs (12%) naquela janela, em sua maioria +1 a 50 linhas em arquivos que já passavam de 2.000 (main.ts, auxiliary_client.py, cron/scheduler.py, o test_tui_gateway_server.py de 23 mil linhas) |
| Relatório da árvore inteira na main | Unidades acima da meta hoje: 451 de CC, 55 de comprimento de função, 28 de aninhamento; 119 arquivos; BLE001 6.516, S110 828, HX012 227, HX006 209, HX005 53 — dívida existente que só bloqueia quando cresce |
A última linha merece um segundo de atenção. O mecanismo não pede que ninguém quite a dívida. As 6.516 exceções engolidas existentes estão congeladas, não agendadas; só importam se crescerem. É isso que torna um portão desses sobrevivível — exigir uma folha em branco no primeiro dia garante que ninguém ligue o interruptor.
O que copiar para o seu próprio repositório
Se o seu time também está deixando agentes escrever bastante código, várias dessas decisões se transferem direto:
- Um ratchet, não uma meta. Não “a CC tem que ficar abaixo de 20 de agora em diante”, mas “o que já está acima não pode piorar, e o código novo tem que atingir o critério”. A primeira versão bloqueia toda edição em código legado.
- Meça na hora, não guarde arquivo de baseline. Sem conflitos entre PRs paralelos e sem o problema de disciplina de lembrar de apertá-lo.
- Rastreie violações de padrão por fingerprint para que uma correção nunca compre uma violação nova.
- Lance regras novas como avisos primeiro, comprove a precisão delas em replays históricos e depois as torne bloqueantes.
- Exija um motivo para toda dispensa e imprima as dispensas — uma exceção silenciosa é o mesmo que regra nenhuma.
- Faça o comando local idêntico ao do CI, para ninguém precisar adivinhar o que a pipeline vai dizer.
Para contexto, este é o mesmo repositório que um agente refatorou em escala em setembro: 1.393 subagentes cortaram 34,4% do Python de não-teste. Isso provou que agentes conseguem reescrever uma base de código em massa. O ratchet responde à pergunta seguinte — como impedir que ela volte a crescer devagar. Se você se importa com restrições de engenharia caindo no CI em vez de numa wiki, as checagens de supply-chain do catálogo de plugins são outra versão de “escreva a regra na pipeline”.
A lição de design
Quem construiu isso fez uma escolha deliberadamente modesta: não perseguir o ideal, só segurar a linha. As metas — CC 20, funções de 300 linhas, aninhamento 6, arquivos de 2.000 linhas — não são um padrão de virtude; são o portão onde o “piorar” para. O que faz isso sobreviver na prática são três detalhes: a dívida existente é congelada em vez de liquidada, o interruptor de enforcement é sempre lido da main e não do branch julgado, e toda regra tem que provar com dados históricos que não vai produzir falsos alarmes. Quando gerar código fica quase de graça, mecanismos como esse ficam mais valiosos, não menos — porque a única coisa que fica mais cara é o tempo que uma pessoa precisa para ler.