mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
150 lines
44 KiB
Markdown
150 lines
44 KiB
Markdown
# AGENT-OPS — operação do paradigma de agentes paralelos
|
||
|
||
Uma página. Norte: simplicidade (GitHub = tracker E Actions — OPS76), agente entrega em `main`, deploy **começa automático** no merge e a produção continua **manual** (reviewer) com verificação full (OPS71 + OPS104).
|
||
|
||
## Ambientes de dados (test ladder — nunca misturar)
|
||
|
||
| Ambiente | Conteúdo | Quem usa |
|
||
| ------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
||
| **Mínimo** (`teqo_test` local / service CI) | schema migrado + `pnpm db:seed:minimal` (sintético, sem PII) | agentes, Cursor Cloud, CI |
|
||
| **Staging** (`teqo_staging`, homeserver) | sintético (`db:seed:minimal`) | job `deploy-staging` do `deploy.yml` (OPS103); inspeção humana antes da aprovação de produção |
|
||
| **Prod** (`teqo_1313`, homeserver) | real | job `deploy-production` do `deploy.yml` (OPS53 + OPS71 + OPS103) |
|
||
|
||
Agentes **nunca** recebem `DATABASE_URL` de prod/staging e nunca setam `ALLOW_REMOTE_DB`. Não há mais smoke `stage` / `ALLOW_STAGE_TEST_DB` / `ci-stage.yml` — o staging da OPS103 é **alvo de promoção de um SHA de `main`** (merge em `main` ou `workflow_dispatch` → staging → aprovação → produção), nunca destino de PR.
|
||
|
||
## Fluxo
|
||
|
||
```text
|
||
claim → feature branch → PR --base main (GitHub) → CI PR green (cascade + skips) → auto-merge nativo em main (rebase)
|
||
main → push dispara deploy.yml sozinho (ou workflow_dispatch manual) → preflight (pula se já há deploy ativo) → verify full verde → deploy-staging → aprovação do environment production → deploy-production; requeue re-despacha se main andou no meio (OPS53 + OPS103 + OPS104 + OPS107 — o approval de produção não ocupa a lane compartilhada; runs novos seguem staging e aprováveis)
|
||
```
|
||
|
||
**Skills:** `plan-issue` (intenção + Issues) → `work-issue` (humano: Issue já claimada → impl plan → confirmação → execução) ou `agent-work-issue` (pool: já claimada → impl plan → execução sem pausa) → `/simplify` → `capture-review-debts` → PR `--base main` (GitHub) → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = Issues do GitHub (`github.com/fsolla/teqo/issues`).
|
||
|
||
**Design hi-fi (OPS113):** item que muda UI passa pelo agente `designer` (`model: openai/gpt-5.6-sol`; crítica contestada/pixel-critical em `openai/gpt-6-astra`, mesmo agente via `/models`); a doutrina `.agents/skills/plan-issue/ui-design-html.md` é a fonte única do artefato `docs/plans/<slug>-ui-design.html` (+ `-ui-design-assets/*.svg`), que é a fonte de verdade do port — o implementador porta classe-a-classe. Indisponibilidade/quota desce a ladder — `opencode-go/deepseek-v4.1-flash` (agente `designer-degraded`) → `deepseek/deepseek-flash` (inline pelo orquestrador) — sempre gravando o tier no PR (`Design tier:`) — ou na Issue, no fluxo autônomo sem tier primário, que não abre PR e flipa para `blocked`; tier degradado marca `DEGRADED`, não certifica e para em sign-off humano (triggers no `work-issue`/`agent-work-issue`: OPS115). Ambos têm `permission` fail-closed: `edit` negado fora de `docs/plans/*-ui-design*` e `bash` com os **vetores de escrita enumerados** negados (redirecionamento, `sed -i`, `tee`, shells/interpretadores — OPS117), preservando leitura e o `prettier --write` do artefato; o resto do shell fica em `ask` (o guard cobre os vetores, não é sandbox de SO). **Escopo de dispatch fechado** (`ui-design-html.md` §Escopo de dispatch): o tier frontier só roda **Criar/Criticar** o artefato de item que muda UI — smoke/validação de `permission`/config, teste de visão, exploração e escrita de plano vão para o **modelo padrão da sessão** (o smoke do guard dos agentes de design roda no `designer-degraded`, barato, pois a semântica de `permission` é do frontmatter, não do modelo); prefira **uma** sessão para N itens UI a N sessões frias; agente de design sem pin (`designer-campanha-solla`) herda o modelo da sessão — dispare a sessão barata e só troque para frontier no design. **Provider `openai` reservado ao design:** nenhum outro agente/sessão — nem `small_model`, compaction ou worker de orquestração — troca para `openai/*` (`gpt-5.6-luna`/`-fast`, `sol-fast`, `terra` inclusive) por `/models`, `--model` ou config; o resto roda no modelo padrão (`deepseek/deepseek-flash`), e a reserva cobre todo o consumo do provider, não só o dispatch do `designer`.
|
||
|
||
- **Agente faz sozinho:** claim (pool-supervisor no pool; humano via `pnpm agent:claim` fora da sessão — OPS33: `worktree next` claima) → implementa → **e2e local afetado** (OPS72, discricionário: roda os e2e que criou + os da mesma superfície — `pnpm test:e2e:affected`; ver skill) → **`pnpm push`** (origin = GitHub) → PR via `node scripts/github-pr.mjs` **Ready** (nunca draft) com `Closes #N` → o safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo do GitHub** (`enablePullRequestAutoMerge`, rebase) — o servidor mergea quando o required check `CI (PR) / checks` fica verde. Regra always-on: `.agents/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false` (o default draft da tool **não** vale neste repo) — o merge continua pelo safety net.
|
||
- **Só humano:** aprovar o environment `production` no run (o único gate de publicação — OPS104 nunca auto-aprova; OPS107: aprove só o run escolhido e rejeite os demais — runs não aprovados não publicam nada), envs de produção no homeserver (`~/stack/teqo-1313.env`, fora do repo — o deploy as lê via BuildKit secrets), `GITHUB_TOKEN` local (PAT) para os scripts GitHub, secrets no repo GitHub, `pnpm configure:branch-protection` (idempotente; reaplicar se o drift voltar), runbook manual/rollback (`docs/ops/teqo-1313-deploy.md`).
|
||
- **Falha do `verify` (OPS106):** o deploy não fica mais parado esperando alguém notar — `auto-unblock.yml` (`workflow_run` no Deploy, só push em `main` e só job `verify` vermelho) dispara UM agente autônomo de desbloqueio no homeserver com o mesmo contrato do `worktree fix` (worktree `fix/*`, banco local `teqo_wt*`, opencode + `/bug-fix`, PR Ready + auto-merge). Single-flight: Issue token `auto-unblock` + `/tmp/teqo-unblock.lock`; falha com agente ativo só comenta no token (nunca enfileira). Guardrails fail-closed de banco/ambiente, cap de 4h, sem auto-retry do deploy e sem auto-approve de produção. Bootstrap/operação: `docs/ops/teqo-1313-deploy.md` (seção Auto-unblock) + `pnpm unblock:check`. O `AUTOMERGE_PAT` é entregue ao processo destacado (push/PR/outcome) por credential helper não-persistente — o built-in token fica só no launcher síncrono.
|
||
|
||
### Dono do PR, dono do CI
|
||
|
||
**Done = PR mergeado em `main` com CI verde.** Falha de CI no teu PR — infra, teste pré-existente ou regressão — é tua até o merge. Exceção de blast radius: migration / access / Consent → pare e escale.
|
||
|
||
### Bypass de CI (cutover / transição)
|
||
|
||
- **Local:** se `gate:push` ainda espelha pipeline antigo e bloqueia push legítimo da PR que o muda → `git push --no-verify` permitido como escape de transição (não WIP permanente). Documentado aqui; após o cutover, entregas passam no CI novo sem bypass.
|
||
- **Cutover humano:** pode relaxar temporary required checks se o flip de proteção travar merges; reverter para `CI (PR) / checks` em `main` assim que o CI novo estiver verde.
|
||
|
||
## Comandos
|
||
|
||
| Comando | Faz |
|
||
| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `pnpm agent:claim [-- --dry-run]` | Fila ready+unblocked por prio → `in-progress` + brief (GitHub) |
|
||
| `pnpm worktree next [--issue N] [--stay]` | Claim determinístico (mesma fila/lock do claim) → worktree + provisionamento |
|
||
| `pnpm worktree fix [bug] [--stay]` | Worktree de correção de bug (`fix/<slug>`, skill `/bug-fix` — o launch envia a descrição do bug); não claima nem cria Issues |
|
||
| `pnpm agent:session <serve\|start\|attach\|stop\|list> [--json]` | Sessões persistentes do run (OPS110 — attach/detach; ver seção abaixo) |
|
||
| `pnpm unblock:check` | Smoke do bootstrap do agente de desbloqueio no homeserver (OPS106; exige `GITHUB_TOKEN` + `AUTOMERGE_PAT`) |
|
||
| `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue no GitHub (`--plan` ⇒ `blocked` até `agent:ready`; sem plano ⇒ `ready`) |
|
||
| `pnpm agent:ready -- --issue N[,N…]` | Pós-merge do plano: `blocked`→`ready` (só Issues com link `docs/plans/`) |
|
||
| `pnpm agent:status` | Overview / fila / mermaid |
|
||
| `pnpm agent:prioritize -- <issue> <P0..P3>` | Troca `prio:*` |
|
||
| `pnpm agent:file-miss -- --title ...` | Issue `kind:agent-miss` |
|
||
| `pnpm agent:pool -- status\|tick --dry-run\|doctor` | Pool Cloud **dormente (OPS65)** — o workflow `agent-pool.yml` foi removido; dispatch falha 404; scripts/skill ficam para histórico |
|
||
| `pnpm push [-- -u origin HEAD …]` | Canônico: ensure-deps + gate:push + push para o **GitHub** (origin) |
|
||
| `GITHUB_TOKEN=… node scripts/github-pr.mjs --head <b> --title <t> --body-file <f>` | Abre PR no **GitHub** (Ready, base `main`, nunca draft) |
|
||
| `GITHUB_TOKEN=… pnpm configure:branch-protection [-- --dry-run]` | Proteção de `main` no **GitHub**: required check `CI (PR) / checks` (idempotente — cria/atualiza/no-op) |
|
||
| `pnpm db:seed:minimal` | DB mínimo sintético |
|
||
|
||
Labels (GitHub): `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, `needs:*`, `auto-unblock` (token do single-flight do agente de desbloqueio — OPS106; fora da fila de claim).
|
||
|
||
**plan-issue (OPS17 + OPS18):** gate pós-overview fecha o lote antes de qualquer Issue/PR; register com `--plan` nasce `blocked` (não entra na fila do claim/pool); após o PR de planos mergear em `main` (`Related #N`), promote dual idempotente: (A) `pnpm agent:ready` na sessão; (B) Action `plan-issue-ready-on-main-merge.yml` lê `Related #N` e promove Issues ainda “aguardando plano” (`blocked` + link `docs/plans/`). Chores sem plano continuam nascendo `ready`.
|
||
|
||
## Sessões persistentes (attach/detach — OPS110)
|
||
|
||
O launch do terminal (`worktree next|plan|new|fix`) **não abre mais um TUI dono da sessão**: ele chama `pnpm agent:session start …`, que sobe/reaproveita o `opencode serve` compartilhado, cria a sessão endereçável, dispara o **driver destacado** (`opencode run --attach … --auto --command <skill>`, que auto-aprova permissões enquanto vive) e **anexa** o TUI (`opencode attach -s <sessionID>`). A sessão nasce na variante `max` (OPS127): ela vai no body do `POST /session` e no argv do `opencode run` do driver/headless — o `opencode attach` não emite flag. Fechar o terminal/TUI desconecta — o run continua; encerrar é ato explícito.
|
||
|
||
| Comando | Faz |
|
||
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `pnpm agent:session serve [--hostname=H] [--port=P]` | Sobe/reaproveita o servidor (loopback `127.0.0.1:4199`; `TEQO_AGENT_SERVER_HOST/PORT`) |
|
||
| `pnpm agent:session start --purpose=<next\|plan\|new\|fix> --dir=<D> --model=<M> [--issue=N] [--argument="<bag>"] [--new]` | Cria a sessão (variante `max` no body — OPS127), dispara o driver (quando há comando a auto-submeter — `plan` sem bag é driverless) e anexa o TUI |
|
||
| `pnpm agent:session attach [--session=<ses_…>\|--branch=<B>\|--issue=N]` | (Re)entra na sessão — default: branch do cwd; sobe o servidor se ele estiver fora |
|
||
| `pnpm agent:session stop [--session=<ses_…>\|--branch=<B>\|--issue=N]` | Ato explícito de encerrar: abort via API + fim do driver + `stoppedAt` |
|
||
| `pnpm agent:session list [--json]` | Status dos runs (`working\|idle\|stopped\|unknown`); `--json` é o contrato do OPS109 |
|
||
|
||
- **Endereçamento (OPS109):** `~/.local/state/teqo/agent-sessions/<slug>.json` (`TEQO_AGENT_SESSION_DIR` > `XDG_STATE_HOME`) guarda `version, code, issue, purpose, branch, dir, sessionID, url, model, driverPid, logPath, startedAt, stoppedAt`; `list --json` entrega `{version, server, sessions:[…status]}` — é a única fonte para "abrir a sessão da Issue".
|
||
- **Sair ≠ encerrar:** fechar o TUI/terminal não mata o run (verificado: SIGKILL no cliente e `tmux kill-session` não abortam; `attach` replayer histórico e streaming ao vivo). `Ctrl+C` no TUI limpa o input; sair é fechar o terminal ou o keybind `app_exit` (`ctrl+c,ctrl+d,<leader>q`).
|
||
- **Reuso:** `start` com driver vivo ou sessão `busy` → apenas anexa (nunca dois runs no mesmo branch); caso contrário cria sessão nova (`--new` força). `worktree kill` encerra a sessão do worktree antes de destruí-lo (estado/lock removidos — sem sessão órfã).
|
||
- **Segurança:** loopback por padrão; `--hostname` fora do loopback **exige** `OPENCODE_SERVER_PASSWORD` (fail-closed) e `--mdns` nunca é usado. A credencial vive só no ambiente — nunca no estado/log; loopback sem senha roda sem auth (aceito pelo produto: nada além de `127.0.0.1`).
|
||
- **Modos de falha:** morte do driver deixa pedidos de permissão pendurados até um `attach` responder (o `stop` sempre aborta via API, sem depender do driver); reboot derruba o run (a sessão/histórico persiste e pode ser reanexada); o daemon do servidor fica de pé (idle, loopback) — parada manual pelo `pid` em `agent-sessions/server.json`.
|
||
|
||
## Contrato de PR
|
||
|
||
1. Schema → atualizar `db:seed:minimal` no mesmo PR (`needs:migration`).
|
||
2. Migrations NÃO são serializadas entre PRs: o CI (migrate + int, incl. `campaignMigrationReconciliation`) valida a cadeia em todo PR; rebase antes de `migrate:create` continua obrigatório.
|
||
3. Ready + auto-merge: `pnpm push -u origin HEAD` (GitHub) → PR via `node scripts/github-pr.mjs` (Ready, base `main`, `Closes #N`) → o safety net `agent-pr-ready-automerge.yml` (`scripts/github-pr-automerge.mjs`) arma o **auto-merge nativo do GitHub por rebase** — o servidor espera o required check `CI (PR) / checks` e mergea; nada a armar por PR, não há `gh` (OPS50) e não há poll (OPS71). **Merge é por rebase** (nunca merge commit): preserva a linha histórica limpa do `main` e é o modo do auto-merge.
|
||
4. **Changelog da entrega (OPS44, OPS85):** toda entrega escreve `docs/changelog/<data>-<id>.md` (ex. `2026-08-13-ops44.md`) — o **único registro commitado** do histórico. O agregado `docs/CHANGELOG-AGENTS.md` é um **artefato gitignored regenerado sob demanda** (OPS85): `pnpm changelog:build` grava o arquivo local, `pnpm changelog:read` imprime no stdout, ambos com seed do snapshot congelado `docs/CHANGELOG-AGENTS-HISTORY.md` quando o arquivo local não existe — **nunca commitar o agregado nem editar o HISTORY na mão**. Guard de CI `docs-guards`: (a) `docs/changelog/` é additions-only (M/D falham); (b) o HISTORY é congelado (só a criação é permitida); (c) o agregado nunca aparece no diff de um PR (a remoção única de OPS85 foi a exceção que criou o HISTORY); (d) nenhum marcador de conflito em diffs de markdown (docs/, AGENTS.md, .agents/ — cobre PRs docs-only, onde a suíte unit não roda). **Desde OPS63 os mesmos checks rodam também no pre-push local** (`gate:push` → `gate:ci`, fase 1 — mesmos scripts, default `origin/main`). Restauração legítima (header muda, restauração estilo D8, correção do HISTORY) escapa escrevendo **`changelog-rewrite: <motivo>` como linha própria** no body do PR — definição canônica aqui; o texto do checkbox do PR template nunca ativa (linha-ancorado). **O escape é CI-only (body do PR):** no pre-push local a restauração legítima falha com a mesma mensagem do CI — bypass documentado é `git push --no-verify` direto (o CI, com o escape no body, continua sendo a via da restauração).
|
||
|
||
## Registries compartilhados (serialização por arquivo, não por PR)
|
||
|
||
Arquivos de "lista" que N agentes escrevem em paralelo (plans, migrações, catalog) conflitam por construção: cada escrita reescreve o arquivo inteiro. Regras:
|
||
|
||
- **Cada entrega escreve UM arquivo por registro** (`docs/plans/<slug>.md`, migration com nome único, etc.) — o arquivo comum é **derivado** (build/CI) ou **só-leitura por PR** (guard). O changelog é o caso resolvido em OPS85: o registro commitado é por-arquivo (`docs/changelog/<data>-<id>.md`), o agregado legível virou artefato regenerado gitignored e o snapshot `docs/CHANGELOG-AGENTS-HISTORY.md` é só-leitura por guard.
|
||
- **`serializes` no plano de intenção** nomeia o registro exato que a Issue toca (ex. `serializes: 'docs/plans/'`), para o executor agendar com clareza — é dica de ordem, não lock (rebase continua obrigatório).
|
||
- O rebase (Contrato de PR item 3) é o mecanismo real de conciliação: conflitos de append resolvem com `git checkout --ours/--theirs` por lado.
|
||
|
||
## Decisões arquivadas
|
||
|
||
- **Docs agent-facing (changelog, plans) NÃO migram para fora do repo (arquivado 2026-08-24, OPS85):** a evidência da OPS85 matou a pergunta — o conflito de merge era do **agregado** (arquivo comum derivado, 171 blocos reescritos no mesmo anchor), não das entradas por-arquivo (imunes por construção desde OPS44) nem dos plans (sempre funcionaram). Migrar 12+ skills com paths hardcoded + o ciclo claim→plano→PR para um alvo externo trocaria conflito de merge por desincronização de registro que os guards não enxergam (anti-goal "segunda fonte de verdade"). **Se um dia houver custo novo que justifique, o alvo registrado é Wiki.js** no homeserver (Node+DB, revisões por página, edição paralela sem git); Outline é segunda opção quando o custo de infra adicional se justificar; Pages estático é a resposta errada para a classe "conflito de git"; coleção `Doc` no Payload acopla docs ao deploy. Nada foi instalado.
|
||
|
||
## CI por alvo (GitHub Actions, OPS71)
|
||
|
||
| Workflow | Trigger | Banco | Passos |
|
||
| ------------------------------------ | --------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `ci-pr.yml` | PR → `main` (same-repo) | 1× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha; `concurrency` nativo cancela o run em push novo; e2e **apenas blast radius** — `selected` ou, em high-risk, o conjunto `curated` (nunca `full` — OPS72/OPS86); área de risco sem mapping falha `unmapped-risk`; unit/int `changed` com seleção vazia cai em full (`vitest-changed-or-full.mjs`) — em 1 processo com `PLAYWRIGHT_WORKERS=4`; 1 service `postgres-int` (localhost:5432) com **`pnpm db:reset`** (drop→migrate→seed:minimal) entre int e build/e2e (OPS88 — substitui o 2º container); fork-PR excluída no `if:` do job (repo público — não queima minutes) |
|
||
| `deploy.yml` | **`push` em `main` (automático, OPS104) + `workflow_dispatch`** | 1× service Postgres 17 | **Cinco jobs (OPS103 + OPS104 + OPS107):** `preflight` (hosted; `scripts/deploy-preflight.mjs` pula o run se já há deploy `queued`/`in_progress` — `waiting` de approval não conta; dispatch manual pula o guard) → `verify` (hosted, suíte **full** sem skips — lint→format→typecheck→knip→cycles→unit→int→`db:reset`→build `.next-e2e`→e2e full; OPS88: um build só, o default saiu) → `deploy-staging` (`needs: [verify]`, `environment: staging`, `TEQO_ENV=staging`) → `requeue` (hosted, **fora** do grupo `deploy-homeserver`; `scripts/deploy-requeue.mjs` re-despacha `deploy.yml` se o head de `main` andou — OPS104) e `deploy-production` (`needs: [deploy-staging]`, `environment: production` com required reviewer, `TEQO_ENV=production`, **sem** `concurrency` desde a OPS107 — o approval não segura a lane, que vive só no staging); ambos os deploys rodam em `runs-on: [self-hosted, homeserver]` e executam `scripts/deploy-homeserver.sh <sha>` **localmente** no homeserver (sem SSH; roda até o fim com o SHA do run mesmo se `main` andar durante o verify — OPS102; idempotente; um `flock` compartilhado é a serialização real entre runs (o `concurrency: deploy-homeserver` com `queue: max` vive só no `deploy-staging` — OPS107); migrator→migrate→runner→rollout→smoke) — staging vermelho fail-closa produção; nada publica sem o approve do reviewer (aprove só o run escolhido; rejeite os demais) |
|
||
| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Closes`/`Fixes` → `done` + `in-prod` + **close** **no GitHub** (token `GITHUB_TOKEN`; body do PR via `PR_BODY` do evento GitHub — no dispatch, busca via `gh` com o mesmo contrato: merged + main + same-repo) |
|
||
| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Related #N` aguardando plano → `ready` **no GitHub** (OPS18; soft-skip; mesmo token) |
|
||
| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `github-pr-automerge.mjs` (plain Node, REST+GraphQL): draft `cursor/*` → Ready (PATCH); arm `enablePullRequestAutoMerge` (rebase) **com o `AUTOMERGE_PAT`** (fail-closed — nunca o `GITHUB_TOKEN` nativo, cujo merge como `github-actions[bot]` não cria runs no `closed`; OPS71-FLIP) — o **servidor** espera o required check `CI (PR) / checks` (o OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto); draft não-`cursor/*` = veto, skip |
|
||
| `archive-cursor-agent.yml` | dispatch manual (ops) | — | Helper: cancela run + arquiva um agente Cursor Cloud (`CURSOR_API_KEY`) — **dormente (OPS65)** como o pool |
|
||
|
||
`agent-pool.yml` foi **removido no OPS65** (pool dormente; o tick `*/10` custava 144 `pnpm install`/dia na workstation). O verificador de main (`ci.yml`) foi **eliminado no OPS71** (gate 2026-08-19): só o `verify` do `deploy.yml` (iniciado por push em `main` ou dispatch manual) roda a suíte full.
|
||
|
||
Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5`, `pnpm/action-setup@v6` e `actions/cache@v4`.
|
||
|
||
Fast gate: `pnpm gate:fast`. Push: `pnpm push`.
|
||
|
||
### Secrets (humano, uma vez — repo GitHub)
|
||
|
||
| Secret | Uso |
|
||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `AUTOMERGE_PAT` | **Arm do auto-merge nativo** (`agent-pr-ready-automerge.yml`) — token de usuário real (fsolla) com `pull-requests: write` + `contents: write` no repo. **Sem ele o job falha fail-closed**: o `GITHUB_TOKEN` nativo armado faria o merge como `github-actions[bot]` e o `closed` do PR não criaria runs (anti-recursão do GitHub) — os flips pós-merge ficariam silenciosamente mortos (OPS71-FLIP, PR #746). **Desde 2026-09-19 o valor é o token OAuth do keyring do `gh`** (`gho_`, longevo — sem os ~30d do PAT fine-grained que expirou em 2026-09-18 e derrubou o safety net): rotacionar/derivar é `gh secret set AUTOMERGE_PAT --body "$(gh auth token)"` (humano). O MESMO token alimenta o `GITHUB_TOKEN` local — `gh auth status` vermelho é o indicador antecedente; revogar/logout do `gh` derruba os dois. Verificação pós-rotação: num PR smoke (commit vazio) o run do safety net deve logar `auto-merge armado` e `gh pr view <n> --json autoMergeRequest` mostrar `enabledBy: fsolla` — desarme com `gh pr merge <n> --disable-auto` e feche o smoke |
|
||
| `CURSOR_API_KEY` | Supervisor do pool (Cursor Cloud) — **dormente (OPS65)**; só o helper `archive-cursor-agent.yml` a usa hoje |
|
||
|
||
Local: `GITHUB_TOKEN` (PAT com escopo `repo` + `issues: write`) para `node scripts/github-pr.mjs`, `pnpm configure:branch-protection`, `pnpm issue`/`agent:*` e qualquer script GitHub — o push SSH usa a chave `~/.ssh/id_ed25519` registrada no GitHub. **PAT expira**: `401 Bad credentials` nos scripts (ou no `gh`) = token vencido/revogado — e um `GITHUB_TOKEN` inválido no ambiente sombreia o login do `gh`; rotacione o PAT ou derive do gh com `export GITHUB_TOKEN=$(env -u GITHUB_TOKEN gh auth token)`. O secret `AUTOMERGE_PAT` do repo deriva do mesmo token do keyring (tabela acima): `gh auth status` vermelho avisa os dois. Envs de produção (DB, S3, VAPID, `REVALIDATE_SECRET`, `NEXT_PUBLIC_SITE_URL`) vivem em `~/stack/teqo-1313.env` no homeserver — nunca como secrets do GitHub. `POOL_GITHUB_TOKEN` é **legado** (era da transição GitHub pré-OPS50). `FORGEJO_API_TOKEN` (git.solla.dev) é **legado da era pré-OPS76** — o tracker migrou para o GitHub e o cliente `forgejo-api.mjs` foi removido.
|
||
|
||
### Flips pós-merge (OPS71-FLIP): o evento `closed` e a recuperação manual
|
||
|
||
O flip `issue-done`/`plan-issue-ready` roda no `pull_request: closed` do PR no GitHub — mas **o merge feito pelo auto-merge armado com o `GITHUB_TOKEN` nativo é atribuído a `github-actions[bot]` e NÃO cria workflow runs** (anti-recursão documentada do GitHub: eventos acionados por ações do `GITHUB_TOKEN` não geram runs; o `closed` fica no timeline do PR com 0 runs — achado ao vivo no PR #746). Isso só não acontece porque o arm agora usa o `AUTOMERGE_PAT` (merge atribuído a usuário real → runs criados, comportamento do PR #742). Desde a OPS104 o mesmo invariante também governa o deploy automático: um merge via `GITHUB_TOKEN` não criaria o run de `push` do `deploy.yml` — o `AUTOMERGE_PAT` é o que mantém trigger 1 (e os flips) funcionando.
|
||
|
||
**Se mesmo assim o flip não rodou** (evento perdido, run falhou, PR mergeado por bot antes do fix): o workflow tem **`workflow_dispatch` de recuperação** — dispatch de `issue-done-on-main-merge.yml` (ou `plan-issue-ready-on-main-merge.yml`) com o input `pr` = número do PR; o job busca o PR via `gh` e aplica o mesmo contrato do evento (merged + base main + same-repo, senão skip). O flip **fecha** a Issue (`PATCH state=closed` — o `closed` do GitHub por keyword pode fechar, mas o flip explicita labels `done`/`in-prod` + close para o tracker GitHub). Falha em qualquer passo = job vermelho (OPS61, nunca mentir).
|
||
|
||
Branch protection de `main` no **GitHub** (aplicada no cutover do OPS71, via `pnpm configure:branch-protection`): required check `CI (PR) / checks` (a UI exibe workflow/job; o **literal de match é `checks`** — o GitHub casa pelo nome do check-run — pin ao vivo PR #742), `strict=false`, 0 reviews, `enforce_admins: true` (nem admin mergea com CI vermelho — mesmo invariante do rule-based Forgejo). O auto-merge nativo espera esse mesmo check antes de mergear; a regra no servidor é o gate real de qualquer caminho de merge, inclusive API manual. Reaplicar/consertar drift: `pnpm configure:branch-protection` (idempotente).
|
||
|
||
Deploy (OPS53 + OPS71 + OPS103 + OPS104 + OPS107): **começa automático** — um `push` em `main` dispara o `deploy.yml` (o job `preflight` pula se já há deploy `queued`/`in_progress`; um run `waiting` de approval **não** conta) e `workflow_dispatch` segue como escape manual. O job `verify` (hosted, suíte full incl. e2e full, **intocado** — verifica o commit uma vez) roda **antes** dos deploys: `deploy-staging` (`environment: staging`, sem reviewer) e então `deploy-production` (`needs: [deploy-staging]`, `environment: production` com required reviewer; staging vermelho nunca deixa produção rodar). Desde a OPS107 o `deploy-production` não tem `concurrency`: o approval pendente não segura o staging nem a aprovação de runs novos — o grupo vive só no `deploy-staging` (aprove só o run escolhido e rejeite os demais). Depois do staging verde, o job hosted `requeue` compara o head de `main` com o SHA do run e re-despacha um run novo se `main` andou no meio — **fora** do grupo de concurrency, para um approval pendente não bloquear a convergência. Os dois deploys executam `scripts/deploy-homeserver.sh <sha>` no runner self-hosted do GitHub **no homeserver** (conecta outbound ao GitHub — funciona atrás do Cloudflare tunnel; o hosted nunca toca o homeserver; o self-hosted não conta minutos hosted), parametrizado por `TEQO_ENV` (`production` default intocado; `staging` = `teqo-staging`/`teqo_staging`/`~/stack/teqo-staging.env`/`127.0.0.1:1314`). O script: flock único no host (compose/workspace/registry são compartilhados) → guard "already deployed" (revision do container do ambiente) → build do migrator (BuildKit, secrets do env file do ambiente) → push/tag em `localhost:5000` → swap dos tags no compose (backup; sed ancorado ao serviço do ambiente) → **migrations** (`teqo-1313-migrate`/`teqo-staging-migrate` — antes do build do runner, OPS66) → build do runner (contra o banco JÁ migrado) → rollout → healthcheck → smoke. Desde a OPS102 o deploy roda até o fim com o SHA do run mesmo se `main` avançar durante o verify; a convergência é do `requeue` da OPS104 (o antigo guard de stale run segue morto); o único verde sem build é o "already deployed", que prova que o container já roda o SHA. Falha = job vermelho; nada é publicado pela metade. Runbook (rollback, falhas conhecidas, bootstrap do staging): `docs/ops/teqo-1313-deploy.md`. O deploy Vercel foi removido no OPS50; o verificador automático de main (`ci.yml`) foi eliminado no OPS71 — a rede de regressão de `main` agora é o `verify` full do próprio `deploy.yml`, que nasce no merge (OPS104 supersede o anti-goal "manual-only" da OPS102: automatiza-se o **início** do run, nunca o approve de produção).
|
||
|
||
## Cursor Cloud
|
||
|
||
`.cursor/environment.json` instala deps + Postgres nativo + `db:seed:minimal`. Sem secrets de prod. Preferir `pnpm push`; escape `git push --no-verify` só no cutover documentado acima. PR via `ManagePullRequest` (draft: false) — o merge continua pelo safety net do GitHub.
|
||
|
||
## Agent pool
|
||
|
||
**Dormente (OPS65)** — o workflow `agent-pool.yml` foi removido e o dispatch
|
||
via CLI falha 404; scripts/skills ficam para histórico. O que foi:
|
||
Supervisor remoto (`agent-pool.yml`): schedule + replenish em merge → `main` +
|
||
dispatch. Workers PR `--base main`. Default model `composer-2.5`. Doctor:
|
||
`pnpm agent:pool -- doctor`. Skill: `agent-pool`.
|
||
|
||
## Guardrails / leitura
|
||
|
||
`pnpm agent:file-miss` → harvest → `docs/GUARDRAILS.md`. Always-on: `AGENTS.md` → codebase-map → engineering-standards → esta página → PRODUCT/DESIGN/CUSTOMER.
|