---
name: steward
description: Guia específica do GarraRUST para dirigir um PR até o verde — quais falhas de CI são ambientais, quais são suas, e o que validar antes de empurrar. Lida ao reagir a eventos de CI ou de review num PR.
---

# Steward — dirigindo um PR do GarraRUST até o verde

Esta skill é lida **no momento em que um PR seu fica vermelho ou recebe
review**. Ela não repete o `CLAUDE.md` (convenções de código, regras absolutas,
estrutura de crates) — vá lá para isso. Aqui está só o que morde nesse momento
específico, e que já custou ciclos de CI reais.

Ela **não** autoriza aprovar nem mergear PR, e não sobrepõe nenhuma regra
"nunca" da política de PR do harness (pular/desabilitar teste, reescrever
histórico de branch alheia, commit vazio para chacoalhar CI).

---

## 1. Antes de qualquer push

O `Format Check` é **bloqueante** e é o erro mais barato e mais frequente:

```bash
cargo fmt --all && cargo fmt --all -- --check
```

Rode isso **sempre**, mesmo em mudança que "só mexe em comentário". `cargo
check`, `clippy` e `test` passando **não** implicam fmt limpo — o rustfmt
reflui `params!`, `assert!`, `match` longos e reordena blocos `use`, e nada
disso aparece nas outras ferramentas.

> Caso real: PR #861 foi para o CI com 21 de 22 checks verdes e o único
> vermelho era fmt — 19 blocos em 8 arquivos, todos cosméticos. Um ciclo
> inteiro de CI (~25 min) perdido.

Depois, o mais próximo possível do que o CI roda:

```bash
cargo clippy --workspace --exclude garraia-desktop --all-targets -- -D warnings
cargo test  --workspace --exclude garraia-desktop
```

`garraia-desktop` é excluído em **todos** os jobs (clippy, build, test, msrv) —
faltam libs GTK e o sidecar Windows nos runners. Não tente "consertar" isso.

---

## 2. Falhas que NÃO são suas

Antes de investigar como defeito, descarte estas. Todas aparecem como falha
legítima e não são.

### `migration_smoke` — "failed to initialize a docker client"

```
Error: failed to initialize a docker client: Socket not found: /var/run/docker.sock
```

Os testes de `garraia-workspace` (e os de integração de `garraia-auth` /
`garraia-gateway`) usam testcontainers. **O container de dev não tem Docker.**
Nos runners Linux do GitHub tem, e lá passam — inclusive aplicando todas as
migrations via `sqlx::migrate!` em Postgres real. Se falhar só localmente com
essa mensagem, é ambiente. Diga isso explicitamente em vez de alegar que
validou.

### `utoipa-swagger-ui` — Swagger UI vem vendorizado (#1228)

O gateway compila o `utoipa-swagger-ui` com a feature `vendored`: o `build.rs`
lê o zip embutido na crate `utoipa-swagger-ui-vendored` e **não baixa nada**,
então build offline funciona sem preparo. O antigo contorno
`SWAGGER_UI_DOWNLOAD_URL=file://...` (zip gerado de um clone da tag) é
ignorado com `vendored` e não resolve mais nada — não perca tempo com ele.

Se um build voltar a tentar baixar o Swagger UI, alguém tirou a feature
`vendored` do `crates/garraia-gateway/Cargo.toml`; o teste
`tests/swagger_ui_vendored.rs` do gateway falha nesse caso. A action
`.github/actions/swagger-ui-cache` é vestigial (fica uma release, download
best-effort) — falha nela não é causa de build quebrado.

### "No space left on device" disfarçado de erro de compilação

`target/` chega a ~26 GB e o disco da sessão é uma cota fixa — `df` engana
(mostra "Avail" baixo com "Used" baixo). Sintoma típico: vários `could not
compile X` sem erro de tipo algum, e no meio um `error 28`. Cure com
`target/debug/incremental` primeiro (metade do peso), `cargo clean` se
necessário. **Commite e empurre antes de limpar**, para que um estouro não leve
o trabalho junto.

### Quality Ratchet reclamando de arquivos grandes

O baseline está congelado em 2026-05-05 e a `main` andou muito desde então.
As regressões de `max_file_lines` / `files_over_*` quase sempre são **deriva do
baseline, não suas**. Confirme antes de agir:

```bash
git diff --name-only origin/main..HEAD          # o arquivo apontado está aqui?
git show origin/main:<arquivo> | wc -l          # já tinha esse tamanho na main?
```

O modo é `report-only` (exit 0) e o check fica verde. **Nunca** edite
`.quality/baseline.json` à mão — é fraude; use `freeze-baseline.py`.

### CodeQL agregado em `neutral`

O check-run agregado "CodeQL" às vezes fecha como `neutral` enquanto os três
`Analyze (rust|actions|javascript-typescript)` fecham `success`. `neutral` não
é falha e não bloqueia. Olhe os três individuais.

### 4 testes de `sandbox` quando o `$HOME` da sessão mora em `/tmp`

```
wrap: Agent("sandbox fail-closed: agent.sandbox.mount_workdir = true e o
diretorio de trabalho e um ancestral do $HOME do processo; ...")
```

Falham juntos, sempre estes quatro de `garraia-agents --lib`:

- `sandbox::shell_injection_regression::wrap_command_entrega_o_comando_como_uma_palavra_literal`
- `sandbox::tests::docker_leva_cap_drop_pids_limit_e_user_do_processo`
- `sandbox::tests::linha_do_bash_leva_nome_do_container`
- `sandbox::tests::podman_leva_userns_keep_id_e_nao_user`

**A guarda está certa e o código não tem defeito** — é o fixture que depende do
`$HOME` da máquina. Os quatro passam `"/tmp"` como `working_dir`, e
`motivo_de_mount_perigoso` recusa qualquer diretório que seja **ancestral** do
`$HOME` do processo (`sandbox.rs:189`). Em runner do GitHub e em dev normal o
`$HOME` é `/home/<user>`, então `/tmp` não é ancestral de nada e os testes
passam. Em sessão cujo `$HOME` é `/tmp/<algo>` — o caso de container de agente —
`/tmp` **é** ancestral do `$HOME`, e o fail-closed do #1272 dispara como
projetado.

Confirme em dois comandos antes de investigar como defeito:

```bash
echo "$HOME"                                    # está sob /tmp?
cargo test -p garraia-agents --lib sandbox::    # só estes 4 vermelhos?
```

Se o `$HOME` está sob `/tmp`, é ambiente. **Não** afrouxe a guarda nem marque os
testes como `ignore` para ficar verde: isso apagaria a regressão de segurança do
#1272. Deixar os quatro herméticos (injetar o `home` por `linha`, como
`fonte_do_mount_com` já permite) é melhoria legítima de teste, mas é mudança em
código de sandbox — território R4, com `security-auditor` no ciclo.

### `STDERR:` na saída de `git` quando o container embrulha o git

```
a chamada benigna revelou o git quebrado no argv do diff: ...
STDERR: Paperclip: GitHub capability_missing; continuing without managed credentials.
```

Atinge `runtime::tests::repo_search_git_e_recall::git_diff_registrada_nao_reabre_ext_diff_pelo_file_path`.

O teste é a regressão do #1269 e afirma que uma chamada benigna de `diff` **não**
produz bloco `STDERR:` — é assim que ele evita ficar verde por acidente com o
git quebrado. O argv continua correto; o que entra na saída da tool é ruído do
host.

A pegadinha: **reproduzir no seu shell não funciona.** Quando o `git` do `PATH`
é um wrapper de credencial (sessão de agente resolve `git` para um launcher
antes de `/usr/bin/git`), ele só escreve o aviso quando **não** consegue falar
com o broker dele. Seu shell interativo tem o token, então `git diff` fica
silencioso; o processo de teste gera o filho com o ambiente higienizado, o token
não vai junto, e aí o aviso aparece. Mesmo comando, dois resultados.

Confirme assim — os dois comandos, não só o primeiro:

```bash
command -v git                  # /usr/bin/git, ou um launcher antes dele?
env -u PAPERCLIP_GITHUB_BROKER_TOKEN git diff 2>&1 >/dev/null
```

Se o segundo imprime `Paperclip: GitHub capability_missing` e o primeiro não
aponta para `/usr/bin/git`, é ambiente. **Não** relaxe a asserção para "não
contém STDERR exceto ...": ela perde exatamente o poder de detecção que
justifica o teste.

---

## 3. Bump do wasmtime: o piso de MSRV vem junto

Já aconteceu **cinco vezes** (1.92, 1.93, 1.94, 1.95, 1.96). Um PR do Dependabot que
sobe `wasmtime` costuma trazer **dois** bloqueios independentes, e o segundo
não aparece se você ler só o erro de compilação:

1. quebra de API em `crates/garraia-plugins/src/runtime.rs`;
2. `MSRV check` falhando **antes de compilar**, porque a árvore
   `wasmtime`/`cranelift-*`/`pulley-*`/`wiggle-*` declara um `rust-version`
   novo.

Corrigir só o código deixa o job de MSRV vermelho. Subir o piso toca
`Cargo.toml` (`rust-version`) **e** o `ci.yml` (nome do job, `dtolnay/rust-
toolchain@X.YZ`, e as duas invocações `cargo +X.YZ`). Prove nos dois sentidos:
o novo piso passa `--locked`, o antigo ainda recusa.

Não confie na sugestão do compilador em migração de API do `wasmtime-wasi`: no
bump 47→48 ele mandava trocar só `FilePerms` por `FsPerms` e **manter**
`DirPerms`, o que não compilaria.

---

## 4. Auto-merge está armado neste repositório

Ele dispara assim que os checks **obrigatórios** passam — o que pode ser
**antes** de todos os checks terminarem, e antes de você chamar merge.

Consequências práticas:

- **Nunca afirme que você mergeou** sem conferir `merged_at` e `merged_by`.
  Uma chamada de merge sobre PR já mergeado retorna "successfully merged" de
  forma idempotente, e o `commit_title` que você passou é ignorado.
- Por isso, **arrume o título do PR antes de ele ficar verde**. O squash herda
  o título do PR, e o auto-merge usa o valor em cache. Um título autogerado
  (`Claude/foo-bar-abc123`) vira assunto de commit na `main`, fora de
  Conventional Commits — e corrigir depois exigiria reescrever a `main`, o que
  é proibido.

---

## 5. O CI não roda em branch solta

`ci.yml` dispara em `push` para `main`/`develop`/`master` e em `pull_request`
para esses alvos. Empurrar uma branch **não** roda nada: sem PR aberto, não há
validação de CI. Planeje isso — e não peça para o usuário "esperar o CI" de uma
branch que não tem PR.

---

## 5a. gitleaks lê o intervalo de commits do PR

O job de segredos varre **cada commit** do PR, não só o diff final. Um
fixture com cara de credencial (chave de teste, token de exemplo) que entrou
num commit e saiu no seguinte continua vermelho — apagar o arquivo não
resolve.

- A saída é uma entrada de **fingerprint exato** em `.gitleaksignore`
  (`<commit>:<arquivo>:<regra>:<linha>`, o que o próprio relatório imprime),
  com um comentário dizendo por que é falso positivo.
- **Nunca** reescreva commit já empurrado para sumir com o achado, e nunca
  afrouxe regra do `gitleaks` para o PR passar. As duas saídas escondem o
  próximo segredo de verdade junto com o falso.
- Credencial real que entrou num commit empurrado é incidente, não falso
  positivo: revogue antes de qualquer outra coisa.

## 5b. Trem de merge: desligue o auto-merge primeiro

Com checks estritos, cada merge em `main` deixa as outras PRs `BEHIND`. Antes
de montar um trem de merge local, **desligue o auto-merge de toda PR
carregada** (`gh pr merge --disable-auto <n>`): uma PR que fica verde no meio
do trem entra sozinha, e o trem passa a conflitar com ela. Na v0.4.3 foi
exatamente isso.

---

## 6. Ao reagir a um review

Achado de bot é relatório de defeito: verifique e corrija se for pequeno e
local. Se os achados pararem de convergir (cada correção gera um novo), pare de
empurrar e levante o assunto uma vez, dizendo o que continua sinalizado.

Pedido grande de revisor humano em PR que não é seu → responda com a proposta,
não empurre nem resolva a thread.

Mudança que toca segurança, auth, storage, RLS, secrets ou CI crítico: o
`CLAUDE.md` manda acionar os agents `security-auditor` e `code-reviewer` antes
de seguir.
