Guia de desenvolvimento
Para o básico (clonar, compilar, rodar os testes, convenções) veja Contribuindo.
Esta página vai além: dois agentes na mesma máquina, o servidor MCP dirigido à mão, diagnóstico de rede,
validação da documentação e a mecânica de um release. Comandos, mapa dos arquivos e invariantes ficam
no AGENTS.md.
Requisitos
| Ferramenta | Para quê |
|---|---|
| Rust 1.91 ou mais novo | mínimo do iroh 1.3 (o job msrv do CI confere); edição 2024 e File::try_lock |
gh (GitHub CLI) | disparar workflows, baixar artefatos, criar releases |
| mdBook 0.5.4 | livro em docs/ (a mesma versão fixada no workflow pages) |
| Chromium | validar diagramas Mermaid e tirar prints do site |
| Python 3 | scripts em scripts/ |
Builds para outras plataformas (macOS, Windows, Linux ARM64 e musl) saem do CI; localmente basta o alvo nativo.
Dois agentes na mesma máquina
Cada perfil é uma identidade numa sala. Com PAPO_HOME apontando para pastas diferentes, dois perfis
convivem sem tocar no seu ~/.papo:
cargo build
PAPO_HOME=/tmp/papo-colega target/debug/papo new --name colega # imprime o convite papo1...
PAPO_HOME=/tmp/papo-voce target/debug/papo join papo1... --name voce
O servidor MCP dirigido à mão
O papo mcp fala JSON-RPC 2.0 pelo stdio, uma mensagem por linha. Abra dois terminais.
Terminal 1, o colega:
PAPO_HOME=/tmp/papo-colega target/debug/papo mcp
Cole estas linhas, uma de cada vez:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"status","arguments":{}}}
Terminal 2, você: o mesmo initialize e notifications/initialized com PAPO_HOME=/tmp/papo-voce,
e depois:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"send","arguments":{"message":"oi, colega"}}}
A sua resposta diz Delivered to colega (msg_id ...), e o terminal do colega imprime a notificação que o
Claude Code receberia:
{"jsonrpc":"2.0","method":"notifications/claude/channel","params":{"content":"oi, colega","meta":{"from":"voce","msg_id":"...","sender_kind":"agent"}}}
Para o colega ler e responder: wait (com timeout_seconds), inbox ou send com reply_to. Fechar o
stdin (Ctrl+D) encerra o servidor do jeito que o Claude Code encerra.
A primeira conexão leva alguns segundos: o endpoint precisa escolher um relay e publicar o endereço antes que o outro lado consiga discar (Pesquisa).
Com o Claude Code de verdade
Um servidor MCP por perfil, então use um perfil e uma pasta de projeto para cada sessão:
papo new --name colega --profile colega
papo join papo1... --name voce --profile voce
cd ~/projeto-a && papo install --profile colega
cd ~/projeto-b && papo install --profile voce
# em cada pasta, num terminal próprio
claude --dangerously-load-development-channels server:papo
Dentro do Claude Code, /mcp mostra se o servidor papo conectou. O que acontecer nessa validação
vai para Status (é o marco M6).
Diagnóstico de rede
PAPO_LOG liga os logs no stderr (o stdout do papo mcp é só do JSON-RPC). Ele aceita a sintaxe de
filtros do tracing-subscriber:
| Filtro | O que mostra |
|---|---|
PAPO_LOG=info | um panorama geral |
PAPO_LOG=papo=debug | as tentativas de discagem do próprio papo (dial failed, connected; handing connection to gossip) |
PAPO_LOG=iroh=info | o relay escolhido (“home is now relay …”) |
PAPO_LOG=iroh::address_lookup=debug | publicação e resolução de endereço pelo pkarr |
PAPO_LOG=iroh_gossip=debug | entradas no tópico, discagens do gossip, vizinhos |
Combinação que resolveu o bug de rediscagem (Problemas conhecidos):
PAPO_LOG=papo=debug,iroh_gossip=debug,iroh=info,iroh::address_lookup=debug \
PAPO_HOME=/tmp/papo-voce target/debug/papo status --timeout 30
O que procurar: start to dial seguido de dial failed: No addressing information available quer dizer
que o outro lado ainda não publicou o endereço (normal nos primeiros segundos, o papo tenta de novo);
nenhum home is now relay quer dizer que a rede bloqueia os relays (tente PAPO_RELAY).
papo status usa uma identidade descartável, então pode rodar ao lado do servidor MCP do mesmo
perfil.
Estado em disco
Tudo de um perfil fica em ~/.papo/profiles/<perfil>/ (ou $PAPO_HOME/profiles/<perfil>/):
| Arquivo | Para olhar quando |
|---|---|
outbox.json | uma mensagem “não chega”: ela está na fila? |
inbox.json | o agente “não vê” uma mensagem: ela foi recebida e ainda não lida? |
log.jsonl | a história toda; papo log -n 100 formata |
peers.json | o nó sabe quem discar? |
lock | “another papo server is already running”: outro processo segura a trava |
profile.json e secret.key guardam o segredo da sala e a identidade: nunca cole o conteúdo deles em
uma issue. O layout completo está em Configuração.
Testes
| Comando | O que roda |
|---|---|
cargo test | unitários + integração com relay local, sem internet |
cargo test --test node | só os nós reais (tests/node.rs) |
cargo test --test mcp | o binário dirigido pelo stdio (tests/mcp.rs) |
cargo test --test mcp -- --ignored | dois servidores MCP pela internet pública |
cargo test --test node queued | um teste pelo nome |
Os testes de integração do nó criam perfis em pastas temporárias com Store::create_at e
Store::open_at, sem tocar em variáveis de ambiente (que são compartilhadas entre as threads de
teste). Os testes do binário usam PAPO_HOME temporário em cada processo filho.
A lista completa de suítes (propriedade, robustez, contrato do MCP, CLI com snapshots, e2e local, fuzzing, benchmarks, cobertura e mutação) está em Contribuindo.
Quality gates
O workflow ci (.github/workflows/ci.yml) reúne todos os checks que barram uma mudança, e o job
agregado quality gate depende de todos eles (needs:). É esse o único check que a main
exige. A tabela do que cada job garante está em
Contribuindo; aqui fica o porquê de cada escolha.
- Um portão só. Checks obrigatórios por job quebram toda vez que um job é renomeado ou criado; o
agregado concentra a regra num lugar. Ele roda com
if: always()e reprova se qualquer dependência terminou emfailure,cancelledouskipped, para nada passar por falha de outro job. - Novo check bloqueante entra como job no
ci.ymle na listaneeds:doquality-gate. Workflows com filtro de caminho (como odocker) não servem como check obrigatório, porque não rodam em todo PR. - MSRV com uma fonte só. O job
msrvlê orust-versiondoCargo.tomle compila com essa versão. Hoje é 1.91 porque o iroh 1.3 e as dependências dele exigem 1.91 (com 1.90 o cargo recusa:iroh@1.3.0 requires rustc 1.91). - Limites do Lighthouse vêm de uma medição local em 2026-10-02 (preset desktop, mediana de 3
execuções): desempenho 100, acessibilidade 100, boas práticas 100, SEO 100, LCP de cerca de 0,4 s e
CLS 0. Os mínimos (90, 95, 95, 90) deixam folga para a variação do runner; contraste de cor é
assertiva própria e reprova sozinha. Os relatórios ficam no artefato
lighthousede cada execução. Configuração em.lighthouserc.json. - Orçamento do site (
scripts/check-site-budget.py) usa gzip nível 9, os mesmos limites da pesquisa de UI/UX (seção 5.7) e um teto de 160 KB para a fonte. Medido em 2026-10-02: HTML 7,9 KB, CSS 7,1 KB, JS 11,8 KB, fonte 139 KB. - HTML válido com o html-validate (regras recomendadas,
.htmlvalidate.json); a única exceção é aceitar o<!doctype html>em minúsculas. - Nomes de pessoas (
scripts/check-names.sh) é a regra do mantenedor virando check: lista de nomes proibidos em palavra inteira, em qualquer arquivo versionado. “Kelvin” fica fora da lista de propósito, porque é o autor e o dono do repositório. - Commits (
scripts/check-commits.py): num PR, conferebase..head; num push namain, os commits novos do push. O histórico inteiro passava quando o check foi criado. - Segredos: gitleaks varre o histórico inteiro (
fetch-depth: 0) com saída redigida. - Workflows e scripts: actionlint (que também passa o shellcheck nos blocos
run:) e shellcheck emscripts/*.she.githooks/*.
Localmente, scripts/quality-gate.sh roda o equivalente de tudo isso (menos o MSRV) e os hooks em
.githooks/ cobrem o mais rápido a cada commit e push. A configuração do check obrigatório no GitHub
é feita uma vez, pelo mantenedor, em Settings > Rules.
Documentação
mdbook serve docs # http://localhost:3000
mdbook build docs # gera docs/book/ (ignorado pelo git)
Validar diagramas
O mdBook não acusa erro de sintaxe Mermaid: o diagrama só quebra no navegador. Antes de commitar um diagrama novo ou alterado:
python3 scripts/check-mermaid.py README.md docs/arquitetura.md docs/engenharia/diagramas.md
O script renderiza cada bloco com o Mermaid 11.4.1 (o mesmo que o livro carrega) num Chromium
headless e falha se algum não renderizar. A armadilha mais comum: o rótulo de uma transição em
stateDiagram vem depois de um : e não pode ter outro (Endpoint::connect quebra o diagrama).
Validar links do livro
mdbook build docs -d /tmp/papo-book
python3 scripts/check-book-links.py /tmp/papo-book
Confere cada href e src locais do HTML gerado, incluindo âncoras. Um link para README.md dentro
do livro quebra (o mdBook transforma esse arquivo em index.html): aponte para a pasta (adr/).
Releases
A parte de processo (quem aprova, o que conferir, o que nunca fazer) está em Releases. A mecânica:
- Atualize
versionemCargo.tomle rodecargo buildpara atualizar oCargo.lock(o CI usa--locked). - Escreva
docs/releases/vX.Y.Z.mdpara quem usa o papo. - Commit, push de
main, depoisgit tag vX.Y.Z && git push origin vX.Y.Z. - O workflow
releasecompila os 5 alvos e o jobpublishcria o release com os arquivos e os.sha256. Hoje ele usa as notas geradas pelo GitHub; passar a usardocs/releases/vX.Y.Z.mdestá no Roadmap.
Para testar os builds sem publicar:
gh workflow run release.yml --ref main
gh run download <id> --dir /tmp/papo-artifacts # os nomes saem como papo-main-<alvo>
Para conferir um arquivo publicado:
sha256sum -c papo-v0.1.0-x86_64-unknown-linux-musl.tar.gz.sha256
tar xzf papo-v0.1.0-x86_64-unknown-linux-musl.tar.gz && ./papo-v0.1.0-x86_64-unknown-linux-musl/papo --version