# Post-mortem: PAT fine-grained expirado no `GITHUB_TOKEN` do shell quebra `pnpm issue`/`agent:*`/`gh` — e o mesmo lote de PAT derruba o `AUTOMERGE_PAT` dos PRs > Template do `/bug-fix`. Preencha com fatos apurados; o que não for apurado fica "não apurado" — nunca invente. ## Registro | Campo | Valor | | ------------------- | ----------------------------------------------------------------------------------------------------- | | Data do post-mortem | 2026-09-18 | | Severidade | alta (quebra a ferramentaria local e o auto-merge de todos os PRs; o deploy de `main` dependia disso) | | Ambiente | dev (workstation) + CI (GitHub Actions) | | Issue(s) | #1169 (C201, `kind:defect` — fechada por este PR) | | PR do fix | #1175 | | Detectado por | log (sessão de agente c194, antes de 2026-09-18) e humano (2026-09-18) | ## Timeline | Momento | Data/hora | Evento | | ------------------ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Início provável | 2026-08-20 (~01:31 UTC) | PAT fine-grained do OPS71 criado no cutover: exportado em `~/.bashrc:194` (comentário "rotacionar após o cutover") e gravado como secret `AUTOMERGE_PAT` (created_at 2026-08-20T01:31:17Z). Nunca rotacionado. Expira ~2026-09 (validade default de 30d) | | Detecção | antes de 2026-09-18 (log) / 2026-09-18 (humano) | Log `~/.local/state/teqo/agent-sessions/c194-…log:74`: `gh issue view` → `HTTP 401: Bad credentials`. Depois o humano reproduziu `pnpm issue next` → 401 | | Detecção (CI) | 2026-09-18T21:39:59Z | Primeira falha do safety net `ready-automerge` com 401 GraphQL (branch `fix/c193-missed-rename`); runs seguintes (`work/18`, `C191-…`, `fix/6`) falham igual — o secret `AUTOMERGE_PAT` está inválido. Sucessos até 20:43:58Z (`plan/c199-c200`) | | Correção do C201 | 2026-09-18T21:52:39Z | O rename perdido do C193 é corrigido de forma independente pelo PR #1171 (`c7d1fd29`), antes deste PR; o deploy run 35398929341 começa no push da `main`. O drive-by deste PR foi dispensado no rebase (trabalho duplicado) | | Correção mergeada | 2026-09-18 | PR #1175 (#1169 é fechada na merge); auto-merge bloqueado até a rotação do `AUTOMERGE_PAT` — merge manual possível com o required check verde | | Deploy | 2026-09-18T21:52:42Z | run 35398929341 (push `c7d1fd29`) em andamento no fechamento deste post-mortem: `verify` full → staging automático → produção após approve humano | | Verificado em prod | não se aplica (bug local de ferramental) | — | ## O bug `pnpm issue next`, rodado de `~/Code/teqo`, falhava com `[agent:issue] GitHub API GET /repos/fsolla/teqo/issues → 401: {"message": "Bad credentials", ...}`. O sintoma atingia toda a ferramentaria local que fala com o GitHub — `pnpm issue`, `agent:*`, `scripts/github-pr.mjs` — além do próprio `gh`, que já havia falhado numa sessão de agente recente (log `c194-…log:74`). Não era runtime de produção; dados de produção intactos. O bash dos agentes herda o env do `opencode serve` (não do `.bashrc`), então os worktrees também quebravam, e os processos de longa duração (`opencode serve`/`web`, `zed`) precisam ser reiniciados para deixar de propagar o valor velho. **Achado no mesmo fluxo (mesma causa-raiz):** o secret de repositório `AUTOMERGE_PAT` — criado na mesma janela do cutover OPS71 — também está inválido. O safety net `agent-pr-ready-automerge.yml` passou a falhar fail-closed com 401 no `enablePullRequestAutoMerge` (GraphQL) a partir de 21:39:59Z, então **nenhum PR consegue armar auto-merge** até a rotação. O valor do secret não é auditável pela API (não apurado se é literalmente o mesmo token do `.bashrc`; os timestamps do lote e a falha simultânea sustentam a hipótese). **Rotação é ação humana** (AGENT-OPS: secrets de repo são "só humano"). ## Causa-raiz 5-whys (401): 1. A API rejeitava o token com "Bad credentials". 2. O ambiente exportava um PAT fine-grained **expirado**; `scripts/lib/github-api.mjs:80` lê `process.env.GITHUB_TOKEN` e usava esse valor. 3. O PAT nunca foi rotacionado: o lembrete vivia só no comentário de `~/.bashrc:194` ("rotacionar após o cutover"), fora de qualquer tracker; PATs fine-grained expiram por padrão em 30d. 4. A expiração foi **silenciosa** — nenhum alerta; o primeiro sinal foi um 401 cru e enganoso. 5. O valor inválido no ambiente **sombreava** a credencial válida do keyring do `gh` (`gho_…`, conta fsolla), porque o `gh` honra `GITHUB_TOKEN` acima do keyring. **Evidência:** `~/.bashrc:194` (pré-fix, PAT adicionado no OPS71 em 2026-08-20); `scripts/lib/github-api.mjs:80`. Reprodução: `gh auth status` acusava o token de env inválido; `env -u GITHUB_TOKEN gh api /repos/fsolla/teqo` funcionava; `GITHUB_TOKEN=$(env -u GITHUB_TOKEN gh auth token) pnpm issue next` funcionava; `GITHUB_TOKEN=ghp_invalid_token pnpm issue next` → 401 com a dica nova. Para o `AUTOMERGE_PAT`: secret criado em 2026-08-20T01:31:17Z (lote do OPS71); primeiras falhas do safety net em 2026-09-18T21:39:59Z; o CLI usa o PAT fail-closed (nunca cai para o `GITHUB_TOKEN` nativo), então o 401 é do secret. Causa-raiz do C201 (registrada porque o fluxo a encontrou): C192 e C193 passaram verdes individualmente, mas o auto-merge por rebase combinou os dois num commit que não foi re-checado; C193 renomeou `canReadSpeechCatalog` → `canReadCommunicationCatalog`, e `src/utilities/speech/speechPageData.ts` manteve o import/call site antigo. Conflito semântico que o merge textual não detecta; a primeira prova foi o `verify` do deploy (run 35394524888) — que só detecta o dano depois de `main` já estar vermelha. ## Correção (1) **Ambiente (fora do repo):** `~/.bashrc` agora **sobrescreve** `GITHUB_TOKEN` com `env -u GITHUB_TOKEN gh auth token` sempre que o `gh` responde (antes do override, o valor herdado por sessões antigas venceria) — escolha do humano; auto-renova enquanto existir o login do `gh` no keyring. Processos de longa duração (`opencode serve`/`web`, `zed`) precisam de restart manual para perder o valor velho herdado. (2) **Repo:** `scripts/lib/github-api.mjs` passa a anexar dica acionável aos erros 401 (REST e GraphQL): `token rejeitado — GITHUB_TOKEN inválido/expirado ou revogado? Rode \`gh auth status\` e rotacione o PAT (um token inválido no ambiente também sombreia o login do \`gh\`).`Dois testes unitários fixam o comportamento (REST 401 sem retry + GraphQL 401) em`tests/unit/githubApi.unit.spec.ts`; `docs/AGENT-OPS.md:124`documenta a expiração de PAT e a recuperação via`gh`. (3) **C201 / #1169:** corrigido de forma independente pelo PR #1171 (`c7d1fd29`, merge 21:52:39Z) antes deste PR; o drive-by equivalente deste branch foi dispensado no rebase. Este PR fecha o item de tracker (#1169), que ficou `OPEN` sem keyword de fechamento no #1171. (4) **Pendente (humano):** rotacionar o secret `AUTOMERGE_PAT` (PAT de usuário real com `pull-requests: write` + `contents: write`) em Settings → Secrets and variables → Actions. Sem ele, o safety net falha fail-closed e nenhum PR arma auto-merge. Enquanto isso, merge manual com o required check verde é o caminho. ## Verificação - Teste de regressão: testes de 401 em `tests/unit/githubApi.unit.spec.ts` — prova RED pré-fix: o módulo (cópia de `HEAD`) emitia `GitHub API GET … → 401: {"message":"Bad credentials"}` sem dica; pós-fix inclui a dica - Suíte: `pnpm test:unit` 364 arquivos / 3869 testes passaram (inclui os 2 novos); `pnpm test:int` 94 arquivos / 838 passaram (2 skipped), após um flake pré-existente e não relacionado de ordem/estado (`testDatabaseLease.int.spec.ts`, verde isolado e em rerun limpo); lint/format/knip/cycles 0; `pnpm typecheck` 0; build 0; `pnpm push` (gate:ci) verde no commit `18216c9f` - A dica funcionou ao vivo no próprio incidente: o log do safety net em 21:57 exibiu `GitHub GraphQL → 401: token rejeitado — GITHUB_TOKEN inválido/expirado…` - Diff sem segredos; `GITHUB_TOKEN=ghp_invalid_token pnpm issue next` sai 1 com a dica acionável; `env -u GITHUB_TOKEN bash -ic 'pnpm issue next'` imprime a Issue #1154 (read-only, confirmada ainda `ready`/unassigned) - C201: `pnpm typecheck` verde na `main` via #1171; deploy run 35398929341 em andamento (a confirmar) - CI do PR #1175: a confirmar no merge; `ready-automerge` falha pelo secret expirado (item 4 acima) - Prod: não se aplica (bug local) ## Prevenção | Estratégia | Custo | Estado | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------- | | Dica acionável no 401 (REST + GraphQL) + 2 testes de regressão + nota em `docs/AGENT-OPS.md` | barata | implementada agora (commit/PR) | | `scripts/github-doctor.mjs` espelhando `db-doctor.mjs` (`GET /user`, imprime login/método), chamado no início de `pnpm issue`/`agent:*`/sessões work-issue — pegaria ESTE bug no início do run | barata | documentada — não implementada neste fluxo | | Calendário/alerta de expiração para os PATs (local e `AUTOMERGE_PAT`) com data de rotação registrada em tracker | barata | documentada — não implementada neste fluxo | | Ciclo de vida de credencial via GitHub App installation token + alerta de expiração em sops/age em vez de `.bashrc`/secret — previne a classe | cara | documentada — não implementada neste fluxo (candidata a Issue futura) | | Guard de conflito semântico de rebase para main (branch protection `strict=true` re-checando o head rebaseado, ou GitHub Merge Queue) — teria pego o main RED de C192+C193 antes do merge | cara | documentada — não implementada neste fluxo (candidata a Issue futura) | **Estratégia implementada:** dica acionável no erro 401 (em vez do "Bad credentials" cru) com testes unitários que a fixam, mais a nota operacional em `AGENT-OPS.md`; o C201 foi corrigido por #1171 e este PR fecha o tracker. **Estratégia documentada (cara):** GitHub App installation token com expiração alertada via sops/age; guard de re-checagem do head rebaseado (branch protection estrita ou Merge Queue); rotação calendarizada dos PATs — nota-se que o `verify` pós-merge do deploy só detecta conflito semântico **depois** de `main` vermelha, e que o `AUTOMERGE_PAT` expirado derruba o auto-merge da frota inteira. Também registrado: **não** fazer fallback silencioso para `gh auth token` dentro de `github-api.mjs` (OPS77 manteve resolução explícita de PAT; fallback silencioso mascara expiração e age como identidade errada). ## Lições Expiração de token é silenciosa: o lembrete existia só como comentário de dotfile fora de qualquer tracker, e o primeiro sinal foi um 401 cru. O mesmo lote de PAT do OPS71 alimentava o ambiente local **e** o secret `AUTOMERGE_PAT` — quando expirou, derrubou a ferramentaria e o auto-merge de todos os PRs ao mesmo tempo, o que só apareceu quando o safety net passou a falhar vermelho. Uma variável de ambiente inválida sombreou uma credencial válida do keyring, produzindo um erro enganoso (a culpa parecia do `gh`/API, não do shell). E dois PRs individualmente verdes podem combinar vermelho em `main` via auto-merge por rebase, sem re-check do commit rebaseado — a primeira prova é o `verify` do deploy, que bloqueia produção até o fix; no mesmo dia, a correção do C201 foi duplicada por dois fluxos (este branch e o #1171), reforçando a importância do claim/single-flight. Trilha de evidências: `~/.bashrc:194` (pré-fix), `scripts/lib/github-api.mjs:80`, commit C193 `bdaf3c7c`, deploy run 35394524888, safety-net run 35399315018.