MÓDULO 2.2

🧾 Subagentes, manifesto e falhas

Coletar em paralelo é rápido; coletar em paralelo sem perder nada exige três peças: um manifesto que não se atropela, um relatório que guarda o que falhou e um verificador que diz "pronto" com número. Aqui estão as três, e os dois bugs reais que o piloto do Nei achou nelas.

6
Tópicos
55
Minutos
2
Casos reais
Prático
Tipo
0%0 de 6
1

🧑‍🤝‍🧑 Um subagente por fonte

No kit, você não roda os coletores à mão um por um: chama a skill /<slug>-coletar e o Claude Code faz a fase 1. A instrução central da skill é "um subagente por fonte, em paralelo". Cada subagente usa só o coletor do seu tipo, devolve uma linha de resultado e some. A sessão principal não se enche de saída de download; ela só junta os resultados e roda o stats.py no fim.

🆕 Novo aqui?

  • Subagente é uma segunda instância do Claude que a sua sessão dispara para uma tarefa isolada. Ele tem contexto próprio e devolve só a conclusão.
  • Em paralelo quer dizer vários subagentes rodando ao mesmo tempo, cada um com a sua fonte.
  • Skill é um arquivo de instruções (.claude/skills/<nome>/SKILL.md) que o Claude Code segue quando você chama /<nome>. A trilha 5 detalha.
  • Trava (ou lock) é um sinal de "ocupado" num arquivo: enquanto um processo segura a trava, os outros esperam a vez.
/slug-coletar sessão principal subagente · live A subagente · live B subagente · guia C subagente · guia D 🔒 travaum por vez MANIFESTO.json 4 itens, nenhum perdido

Como ler: o leque da esquerda é o paralelo (cada caixa pulsando é um subagente trabalhando ao mesmo tempo). O funil do meio é a trava: o download acontece em paralelo, mas a escrita no manifesto é em fila. Sem essa caixa do meio, as quatro setas chegariam juntas e algumas linhas sumiriam (tópico 5).

✓ Por que um subagente por fonte

  • ✓Uma fonte lenta (transcrição) não segura as rápidas (legenda)
  • ✓A saída barulhenta do download fica fora da sessão principal
  • ✓Uma falha fica isolada na sua fonte

✗ O que o paralelo cobra

  • ✗Todo mundo escrevendo no mesmo arquivo ao mesmo tempo
  • ✗Arquivos com o mesmo nome se sobrescrevendo
  • ✗Ninguém olhando o conjunto no fim (por isso o stats.py)
2

📒 Leia o MANIFESTO.json e o sha256

O manifesto é a lista oficial do acervo. Cada coletor, ao terminar uma fonte, acrescenta um item. Este é um item real do mentor do Nei, exatamente como está no arquivo:

{
  "id": "guia-maestro-roteador-triagem-de-modelo-e-esforco-para-ded1b",
  "tipo": "guia",
  "url": "https://inematds.github.io/maestro-roteador/guia/",
  "titulo": "maestro-roteador — triagem de modelo e esforço para Claude Code",
  "arquivo": "raw/guia/maestro-roteador-triagem-de-modelo-e-esforco-para--ded1bc.md",
  "coletado_em": "2026-10-05",
  "palavras": 1418,
  "sha256": "b8540f7c45b432d3779817b05f659e1a84a82a9d46c33d38a00d8ecc0b351039"
}

🆕 O que é sha256?

É uma impressão digital do arquivo: um código de 64 caracteres calculado a partir de cada byte do conteúdo. Mude uma vírgula e o código muda inteiro. Por isso ele serve de lacre: o coletor grava o sha256 na hora da coleta, e o stats.py recalcula depois. Se não bater, alguém editou o raw/ — e isso é proibido.

CampoPara que serve
urlChave de "já coletado": a mesma URL não é baixada de novo.
arquivoOnde está o texto. Também é a chave de atualização: coletar de novo o mesmo arquivo substitui o item.
palavrasSomadas por tipo, viram o placar contra as metas.
sha256Lacre do raw. A wiki (trilha 3) e as citações (trilha 4) dependem dele.
coletado_emData da coleta, para a ingestão contínua saber o que é novo.

🧪 Exercício copiável — confira o lacre à mão

Objetivo: ver o sha256 funcionando sem depender do stats.py. Rode na raiz do seu mentor (ou de um clone do mentor-nei).

python3 - <<'EOF'
import hashlib, json
for i in json.load(open("raw/MANIFESTO.json")):
    atual = hashlib.sha256(open(i["arquivo"], "rb").read()).hexdigest()
    print("ok   " if atual == i["sha256"] else "MUDOU", i["palavras"], i["arquivo"])
EOF

Como verificar: todas as linhas começam com ok. Para ver o lacre denunciar, acrescente um espaço num arquivo do raw numa cópia descartável do projeto e rode de novo: aquela linha vira MUDOU.

3

🚩 Mantenha o RELATORIO-FALHAS.md

Uma coleta que só mostra sucessos esconde o que ficou de fora. Por isso todo coletor, ao falhar, escreve uma linha em raw/RELATORIO-FALHAS.md: data, tipo, origem e motivo. O relatório do piloto tem uma única linha, e ela conta uma história inteira:

# Relatório de falhas da coleta

| data | tipo | origem | motivo |
|---|---|---|---|
| 2026-10-05 | video | https://www.youtube.com/watch?v=Zb8TRf97zt8 | sem legenda |

📊 O que aconteceu com essa linha

  • • A live "Agentes com LLM grátis" não tinha legenda em pt nem em pt-orig, e o transcrever_cmd ainda estava vazio.
  • • O coletor fez o certo: registrou "sem legenda" e seguiu com as outras 13.
  • • O Nei leu o relatório, preencheu o transcrever_cmd (módulo 2.1, tópico 3) e a live entrou: 8.045 palavras em 7,7 min.
  • • A linha continua no relatório. Ele é um histórico do que foi olhado, não uma lista de pendências que se apaga.

✓ Faça com uma falha

  • ✓Leia o motivo e decida: transcritor, coleta manual ou desistir
  • ✓Rode o coletor de novo depois de resolver
  • ✓Crie o arquivo mesmo sem falha nenhuma (prova que foi olhado)

✗ Não faça

  • ✗Apagar a linha para o relatório "ficar limpo"
  • ✗Colar o texto à mão dentro de raw/ sem passar por um coletor
  • ✗Dar a coleta por pronta sem abrir o relatório
4

🎯 Defina "pronto" com metas e stats.py

"Acho que já coletei bastante" não é critério. No kit, a fase 1 está pronta quando python3 tools/stats.py sai com exit 0. As metas ficam em metas_coleta no config, por tipo, em itens e em palavras. As do piloto eram estas:

"metas_coleta": {
  "video": { "itens": 10, "palavras": 60000 },
  "guia":  { "itens": 5,  "palavras": 8000 }
}

🆕 O que é exit code?

Todo programa de terminal termina com um número. 0 quer dizer "deu certo"; qualquer outro, "falhou". Você vê o número com echo $? logo depois do comando. É por esse número, e não pelo texto bonito, que o agente e você decidem se a fase acabou.

O stats.py reprova se…

manifesto vazio

Nada foi coletado

sem sha256

Item sem lacre

arquivo sumiu

Manifesto aponta para o nada

sha256 diferente

Raw alterado depois da coleta

meta não batida

E diz quanto falta (desde o kit 1.2)

sem relatório

RELATORIO-FALHAS.md tem de existir

A fase 1 do piloto, hora a hora

15:50

Primeira rodada: reprovado

13 lives somaram 169.937 palavras (meta de vídeo batida com folga). Mas 6 guias deram 7.269 palavras contra a meta de 8.000. stats.py → exit 1. O agente reportou o exit 1 sem maquiar e sugeriu fontes extras.

16:05

Correção: +1 guia e a transcrição

Entrou mais um guia e a live sem legenda foi transcrita localmente. Resultado: 21 itens, 187.382 palavras, stats.py → exit 0. Fase 1 aprovada.

17:00

Depois: ingestão

No teste 3, um guia novo foi ingerido (2.024 palavras). O acervo fechou em 22 fontes, 14 lives e 8 guias, 189.406 palavras. A trilha 6 mostra essa ingestão.

A reprovação das 15:50 no formato do stats.py 1.2 (reconstrução com os números reais)

python3 tools/stats.py
tipo          itens   palavras   meta
guia              6       7269   ≥5 itens / ≥8000 pal.
video            13     169937   ≥10 itens / ≥60000 pal.
TOTAL            19     177206

REPROVADO:
  - meta não batida em 'guia': faltam 731 palavras

Como ler: a linha de guia tem itens de sobra (6 contra 5) mas palavras de menos. A lição do piloto foi calibrar a meta depois de listar as fontes: guias INEMA têm cerca de 900 a 1.400 palavras, então 8.000 pede uns 7 guias, não 5.

5

🔒 Caso real 1: o agente recusou o paralelo

No kit 1.1, a skill mandava coletar com um subagente por fonte em paralelo. Na sessão do piloto, o agente não obedeceu — e estava certo. Ele leu _comum.py, viu que registrar_item lia o manifesto inteiro, acrescentava um item e regravava o arquivo, sem trava nenhuma. Rodou os coletores em sequência e explicou o porquê. O diário marcou: "paralelo perderia entradas".

🆕 O que é "ler-alterar-gravar" sem trava?

É o padrão "abre a lista, põe um item, salva a lista". Com um processo só, funciona. Com dois ao mesmo tempo, os dois abrem a mesma versão antiga, cada um põe o seu item, e quem salva por último apaga o item do outro. Chama-se condição de corrida: o resultado depende de quem chega primeiro.

tempo coletor A coletor B lê: 5 itens lê: 5 itens grava: 5 + A = 6 grava: 5 + B = 6 manifesto: 6 itens o item A sumiu

Como ler: B lê o arquivo antes de A gravar, então B não sabe que A existe. Quando B grava, escreve "5 antigos + B" por cima de "5 antigos + A". Deviam ser 7 itens; ficaram 6. Ninguém recebe erro: a perda é silenciosa, e é por isso que o agente acertou ao parar.

O kit 1.2 corrigiu com duas proteções em _comum.py: uma trava de arquivo (fcntl.flock em raw/.manifesto.lock) em volta do ler-alterar-gravar, e escrita atômica: grava num arquivo temporário e troca de uma vez com os.replace, para que ninguém leia um manifesto pela metade. O trecho real:

def _gravar_atomico(destino: Path, conteudo: str) -> None:
    fd, tmp = tempfile.mkstemp(dir=destino.parent, prefix=".tmp-", suffix=destino.suffix)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        f.write(conteudo)
    os.replace(tmp, destino)

# dentro de registrar_item(...)
    with trava(raiz):
        itens = [i for i in ler_manifesto(raiz) if i.get("arquivo") != rel] + [item]
        _gravar_atomico(raiz / "raw" / "MANIFESTO.json", json.dumps(itens, ensure_ascii=False, indent=2) + "\n")

🧪 Exercício copiável — provoque o paralelo

Objetivo: repetir o teste de regressão do kit: 12 coletas ao mesmo tempo, 12 itens no manifesto. Rode num mentor descartável gerado pelo kit 1.2 ou superior.

python3 novo-mentor.py teste --nome x --dominio y --destino /tmp/paralelo
cd /tmp/paralelo/mentor-teste
for i in $(seq 1 12); do yes "texto $i" | head -50 > /tmp/paralelo/f$i.txt; done
for i in $(seq 1 12); do python3 tools/coletar_arquivo.py /tmp/paralelo/f$i.txt --tipo notas & done; wait
python3 -c "import json;print(len(json.load(open('raw/MANIFESTO.json'))))"

Como verificar: o último comando imprime 12. É o mesmo que o teste test_manifesto_nao_perde_itens_com_coleta_em_paralelo do kit afirma. O primeiro comando roda na pasta do kit (onde está o novo-mentor.py).

💡 A lição que vale para fora do kit

Instrução ("rode em paralelo") e mecanismo (o código que grava) precisam concordar. Quando não concordam, o melhor que pode acontecer é o agente perceber e parar, como aqui. O pior é ele obedecer e você descobrir semanas depois que faltam fontes no acervo.

6

🏷️ Caso real 2: o transcript.txt que sobrescrevia

Às 16:05, a transcrição local da live sem legenda funcionou: 8.045 palavras em 7,7 minutos. Mas o arquivo foi salvo como raw/videos/transcript.txt — o nome que o transcritor deu, não um nome do vídeo. Na próxima live transcrita, o transcritor geraria outro transcript.txt, o coletor gravaria por cima, e o manifesto trocaria o item antigo pelo novo (lembre: o arquivo é a chave de atualização). Uma fonte sumiria sem aviso. O diário marcou gravidade alta.

kit 1.1 · nome do transcritor kit 1.2 · título + ID do vídeo live 1 live 2 transcript.txtsó sobra a live 2 live 1 live 2 <titulo-1>-<ID1>.txt <titulo-2>-<ID2>.txt sem ID? usa os 8 primeiros do sha1 da URL

Como ler: à esquerda, duas setas vermelhas chegam na mesma caixa: duas fontes, um arquivo. À direita, cada live tem a sua caixa, porque o nome vem do próprio vídeo (título e ID lidos com yt-dlp --print, sem baixar nada). Se o vídeo não tiver ID, o sufixo vem de um hash da URL — duas URLs diferentes nunca dão o mesmo nome.

A correção é uma linha de coletar_video.py, com o comentário que conta a história:

# nome pelo vídeo (título + id), nunca pelo arquivo do transcritor: "transcript.txt" sobrescrevia o anterior
titulo = titulo or info
sufixo = vid or hashlib.sha1(url.encode()).hexdigest()[:8]
destino = raiz / "raw" / "videos" / f"{slugificar(titulo, 50)}-{re.sub(r'[^A-Za-z0-9_-]', '', sufixo)[:20]}.txt"

⚠️ A prova ainda está lá

No repositório do mentor-nei, o manifesto continua com um item raw/videos/transcript.txt de 8.045 palavras. Ele não foi renomeado de propósito: o raw é imutável e o lacre sha256 está valendo. A correção vale para as próximas coletas; o acervo antigo fica como evidência do bug.

🧪 Exercício copiável — procure nomes genéricos no seu acervo

Objetivo: descobrir se algum arquivo do seu raw tem nome que não identifica a fonte (sinal de que um coletor antigo ou uma cópia à mão passou por ali).

ls raw/videos | grep -Ei '^(transcript|output|audio|video|legenda)[^a-z]*\.(txt|srt|vtt)$' \
  || echo "nenhum nome genérico"

Como verificar: num mentor coletado com o kit 1.2 ou superior, a saída é nenhum nome genérico. No mentor-nei, o comando lista transcript.txt — o caso deste tópico.

Checagem rápida: por que o bug do transcript.txt não dava nenhum erro na tela?

📌 Resumo do Módulo

✓
Um subagente por fonte - download em paralelo, escrita no manifesto em fila.
✓
Manifesto com lacre - cada item tem url, arquivo, palavras e sha256; raw alterado é detectado.
✓
Falha tem endereço - RELATORIO-FALHAS.md existe sempre, mesmo vazio, e não se apaga.
✓
Pronto é exit 0 - no piloto: 731 palavras de guia faltando → +1 guia e a transcrição → 21 itens, 187.382 palavras.
✓
Caso 1 - o agente recusou o paralelo sem trava; o kit 1.2 ganhou flock + escrita atômica.
✓
Caso 2 - transcript.txt sobrescrevia; agora o nome é título + ID do vídeo.

Próximo Módulo:

3.1 - Raw imutável, wiki viva: por que o acervo cru não serve como memória, e como compilar em vez de copiar.