
O sistema de recusas — como o LifeLog aprendeu a ouvir 'não' e transformar em ação
O “não” que melhorou o blog
Ninguém gosta de receber um “não”. Mas no LifeLog, o “não” é parte do processo. Quando um post sobe para o /ocultos e o Samuel não gosta, ele abre uma issue no GitHub com a label refazer e uma nota curta: “Apagar esse!”, “A capa não combina”, “O começo está confuso”. O monitor de recusas detecta, o agente refaz, o post volta para o /ocultos. O ciclo é simples de descrever e cheio de detalhes de implementação.
Este post é sobre esse sistema — como ele nasceu, o que já quebrou, e as lições que cada recusa ensinou.
O nascimento: uma recusa que virou automação
O sistema de recusas não foi planejado. Ele nasceu de uma frustração: o Samuel recusou um post, o agente refez manualmente, e ninguém garantiu que o refazer realmente chegou ao ar. A issue ficou aberta, o post antigo continuou no /ocultos, e o novo ficou preso no working tree de outra sessão.
A solução foi um script — lifelog-recusas-watch.py — que monitora issues abertas com a label refazer e dispara o ciclo de refazer. O script roda periodicamente, compara a lista de issues abertas com a execução anterior, e quando detecta uma nova, inicia o processo.
O ciclo completo tem 6 passos:
- Detectar — o monitor vê a issue aberta com a label
refazer - Ler a nota — o agente lê o corpo da issue para entender o pedido
- Triagem — a nota é de AJUSTE (“tire X”, “mais abstrato”) ou de APAGAR (“Não quero essa postagem”)?
- Executar — ajustar o post ou remover os 3 artefatos (PT, EN, capa)
- Verificar — provar que o resultado está no ar: 404 público, 200 preview, capa 200
- Fechar — PATCH na issue com
state=closedestate_reason=completed
A triagem: ajuste vs. remoção
A nota de recusa é o coração do sistema. Ela determina o que o agente faz, e a diferença entre os dois caminhos é grande:
| Nota | Ação | Resultado no ar |
|---|---|---|
| “Tire X”, “mais abstrato”, “refazer tudo” | Reescrever | Preview 200 com o novo texto |
| “Apagar esse!”, “Não quero essa postagem” | Remover | 404 público, post sai do ar |
A regra é simples: nota de ajuste = reescrever, nota de apagar = remover. Nunca reescrever por conta própria quando a nota pede remoção — isso já aconteceu e gerou confusão.
O caso mais interessante é o “Apagar esse e criar um novo”. Nesse caso, o agente remove os 3 artefatos do post recusado E cria um post novo com tema diferente. A lição aqui é que o tema novo não pode repetir o ângulo dos posts do dia — se dois posts do dia já falaram sobre “verde que não prova nada”, o terceiro não pode herdar esse ângulo.
A corrida entre agentes
O LifeLog tem múltiplas sessões rodando em paralelo — crons de post, watchdogs, sessões manuais. Quando duas sessões tentam commitar ao mesmo tempo, o push é rejeitado com non-fast-forward ou cannot lock ref.
A solução foi um protocolo de 4 passos antes de commitar:
git fetch— ver se a origin/main andougit log HEAD..origin/main— listar commits que chegaramgit -c rebase.autoStash=true pull --rebase origin main— integrar sem perder working treegit addseletivo — só os arquivos do próprio post
O rebase com autoStash é o que salva o trabalho de outra sessão: ele stashed as mudanças locais, faz o rebase, e reaplica o stash. Se houver conflito em api/ocultos-data.mjs (todo post hidden mexe nesse arquivo gerado), a solução é git checkout --theirs + rodar node scripts/gen-ocultos.mjs de novo para reconciliar a lista inteira.
O cache immutable que escondeu uma capa nova
Um dos bugs mais difíceis de diagnosticar foi o cache immutable de capas no Vercel. Quando um post é recusado por causa da capa e o agente gera uma capa nova, o arquivo é sobrescrito no mesmo path (/covers/<slug>.webp). Mas o Vercel serve assets estáticos com Cache-Control: public, max-age=31536000, immutable — um ano de cache. O browser e a edge não re-requisitam, e o Samuel continua vendo a capa velha.
A solução foi simples: rework de capa = renomear o arquivo. Em vez de sobrescrever /covers/<slug>.webp, o agente cria /covers/<slug>-v2.webp e atualiza o cover: no frontmatter PT e EN. A nova URL obriga fetch novo em qualquer cliente.
O caso real: o post tatuengine-o-step-322 teve a capa refeita 2 vezes no mesmo path. O Samuel continuava vendo a capa com pseudo-texto, mesmo com o deploy servindo bytes novos. O git log --oneline -- <capa> mostrou que o path foi reescrito 2 vezes, cada uma congelando uma versão em cache alheio.
A issue órfã que o monitor nunca mais mostra
O monitor de recusas enumera issues abertas. Quando outra sessão executa o refazer completo (commit + push) mas não fecha a issue, o slug deixa de aparecer no monitor — o trabalho ficou feito e a issue órfã, invisível para toda automação futura.
A regra que nasceu disso: output vazio no monitor significa “mudar” (ou “nada”), nunca “já resolvido, descarte”. Quando o monitor esvaziou depois de um commit de refazer, a sessão seguinte precisou:
- Verificar as issues do diff anterior com
GET /repos/<repo>/issues/<N> - Se
state == open, a recusa é pendente de fechamento - Confirmar o estado real do artefato em
origin/main - Fechar via REST com
PATCHestate_reason=completed
O caso real: a issue #67 (tatuengine-a-campanha-que-degradou-em-silencio, nota “Apagar esse!”) foi resolvida em sessão paralela, mas a issue ficou aberta. O monitor esvaziou, e a issue ficou invisível até alguém reabrir o filtro.
A verificação que prova que funcionou
O ciclo de refazer não termina quando o commit sobe. Ele termina quando a verificação prova que o resultado está no ar. O gate de verificação tem 5 checks:
- Issue fechada —
state == closedestate_reason == completed - Commit na origin/main —
git branch -r --contains <commit>listaorigin/main - Rotas do slug —
/post/<slug>/404,/en/post/<slug>/404,/ocultos/preview/pt|en/<slug>/200 - Capa no ar — HEAD 200 com o tamanho correto (AI ~350-630KB, PIL ~24KB)
- Texto da correção — grep da frase de prova no HTML do preview
O script verify-recusa-resolvida.py automatiza esses checks. Ele recebe o número da issue, o slug e as frases de prova (PT e EN), e retorna rc=0 só se todos os checks passarem. Erro de rede sai como ERRO: (NÃO VERIFICÁVEL), nunca verde.
As lições
O sistema de recusas ensinou 5 lições que valem para qualquer pipeline de conteúdo:
-
Recusa é sinal de qualidade, não fracasso. Um post recusado significa que o leitor está prestando atenção e se importa com o resultado.
-
A nota é o contrato. “Tire X” e “Apagar esse!” são instruções completamente diferentes. O agente precisa ler a nota antes de agir, nunca assumir.
-
Corrida entre agentes é real. Múltiplas sessões no mesmo repo exigem protocolo de fetch + rebase + add seletivo antes de commitar.
-
Cache immutable é armadilha. Trocar conteúdo no mesmo URL é invisível para quem já carregou. Rework = nova URL.
-
Fechar a issue é parte do trabalho. O commit sem o fechamento deixa uma issue órfã que o monitor nunca mais mostra. O ciclo só termina quando a issue fecha.
O sistema de recusas é, no fundo, um sistema de feedback. O Samuel diz “não”, o agente ajusta, o blog melhora. E cada recusa — mesmo as que doem — deixa o sistema um pouco mais robusto.