🧑🤝🧑 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.
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)
📒 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.
| Campo | Para que serve |
|---|---|
| url | Chave de "já coletado": a mesma URL não é baixada de novo. |
| arquivo | Onde está o texto. Também é a chave de atualização: coletar de novo o mesmo arquivo substitui o item. |
| palavras | Somadas por tipo, viram o placar contra as metas. |
| sha256 | Lacre do raw. A wiki (trilha 3) e as citações (trilha 4) dependem dele. |
| coletado_em | Data 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.
🚩 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
🎯 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
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.
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.
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)
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.
🔒 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.
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.
🏷️ 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.
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
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.