mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
refactor(OPS97): fixes do /simplify — fail-closed em PR inexistente, errata do plano, token neutro na skill
This commit is contained in:
parent
eec6fd3d49
commit
fca5a1c105
5 changed files with 26 additions and 10 deletions
|
|
@ -50,7 +50,7 @@ Tudo aqui é leitura. Falhou um item → pare com o remédio nomeado, não impro
|
|||
3. Branch corrente ≠ `main`. Se estiver em main/detached, crie `agent/<N>-react-audit`
|
||||
a partir de `origin/main` — nunca commitar fix em main.
|
||||
4. `GITHUB_TOKEN` presente e validado com chamada barata ANTES do trabalho:
|
||||
`node -e "import('./scripts/lib/github-api.mjs').then(m=>m.githubApi.getIssue(901)).then(i=>console.log(i?.state))"`.
|
||||
`node -e "import('./scripts/lib/github-api.mjs').then(m=>m.githubApi.listIssues({limit:1})).then(i=>console.log(i.length>=0))"`.
|
||||
5. Baseline bare dos gates estáticos — vermelhos pré-existentes são BASELINE (política
|
||||
"dono do PR, dono do CI", `docs/AGENT-OPS.md`), anote-os:
|
||||
`pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm exec knip`, `pnpm check:cycles`.
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ triage per-fix (3 selos: file:line, família, fonte oficial; cap ≤6) → imple
|
|||
bare e um commit por fix → PR único Ready **sem auto-merge** cuja descrição é o relatório consolidado,
|
||||
estabilizado até checks green ∧ mergeable ∧ auto_merge null — a skill para sem mergear (humano revisa).
|
||||
Mecânica anti-auto-merge: PR nasce uma única vez já estabilizado e o desarme verificado em laço
|
||||
(`githubApi.ensureAutoMergeDisabled`, novo helper GraphQL `disablePullRequestAutoMerge` +
|
||||
re-poll em `scripts/lib/github-api.mjs`, com `auto_merge` normalizado em `getPullRequest`) é
|
||||
(`githubApi.ensureAutoMergeDisabled`, novo par GraphQL `disableAutoMerge`/`enableAutoMerge` em
|
||||
`scripts/lib/github-api.mjs`, com `auto_merge` normalizado em `getPullRequest`) é
|
||||
obrigatório após a abertura E após cada push — o safety net `agent-pr-ready-automerge.yml`
|
||||
re-arma a cada synchronize, e o endpoint REST DELETE responde 404 mesmo armado-via-GraphQL
|
||||
(corrida e 404 capturados ao vivo no dogfood #905).
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ Alternativas rejeitadas: B porque cria segunda identidade para o mesmo trabalho
|
|||
- **`.agents/skills/react-audit/anti-patterns.md`** (novo): catálogo FIXO, uma seção por família com `Sintoma/heurística de busca`, `Norma no repo` (âncora nos .mdc), `Fonte oficial` (URL react.dev/learn, react.dev/reference, nextjs.org/docs, nextjs.org/learn — conferidas contra next 15.4.11 / react ^19.2.4), `Receita de fix`, `Precedente` (B14 ~21 kB sidebar, B34 closures/fallbacks obsoletos, B32+ live-region, C139/C140 reset de form React 19 em `docs/plans/react-19-form-reset-campanha.md`, P4-F). Famílias fechadas (9): (1) client boundary/server-first (`'use client'` desnecessário, import de server module por VALOR, estado elevado além dos consumidores, view models não selecionados); (2) RSC payload bloat (doc Payload inteiro no fio); (3) effects fazendo derived-state / stale closures (B34); (4) forms & server actions (reset `action={submitAction}`, transição ausente, optimistic sem `revalidatePath`); (5) caching ladder (live data sem invalidação, auth dentro do core cacheado); (6) URL serializers chegando ao browser (B14); (7) live-region mistakes (B32+); (8) loading/streaming feedback (dim o resultado, não o controle; `<Suspense>`/`loading.tsx`); (9) next/image + fonts + a11y de componentes.
|
||||
- **`.agents/skills/react-audit/context7-setup.md`** (novo): passo MANUAL do dono — declarar o MCP globalmente FORA do git em `~/.config/opencode/opencode.jsonc` (bloco `mcp` remote `https://mcp.context7.com/mcp`), validar com `opencode debug config`; deixado claro que a skill funciona sem (fallback = links oficiais embutidos no catálogo); o executor valida o shape vigente do bloco ao escrever o doc.
|
||||
- **`scripts/lib/github-api.mjs`** (editar o dono — é o dono do acesso REST GitHub): adicionar `disablePullRequestAutoMerge(number)` (DELETE `/repos/{owner}/{repo}/pulls/{number}/auto-merge`, tolerando 404) e expor `auto_merge` no retorno de `getPullRequest`; reuso integral do que já existe (`createIssue`/`listIssues` para follow-ups, `mergeable` para estabilização). Único código novo da entrega; sem twin em curl/scripts avulsos.
|
||||
- **Errata de execução (2026-08-25):** o mecanismo acima foi superseded pelo dogfood #905 — o endpoint REST DELETE responde **404 mesmo com auto-merge armado via GraphQL**, e o armar do safety net é assíncrono (um desarme único perdeu a corrida). Entregue no lugar: `disableAutoMerge(nodeId)` por mutation GraphQL `disablePullRequestAutoMerge` (simétrica ao `enableAutoMerge`) + `ensureAutoMergeDisabled(number)` com laço desarme→re-poll até `auto_merge = null`, fail-closed em PR inexistente.
|
||||
- **Migration:** sem migration. **Access/Consent:** N/A (nenhuma collection/global tocada; fixes de produto continuam sujeitos às regras normais quando tocarem essas áreas — aí viram follow-up, não fix noturno). **UI:** Impeccable A — N/A sem UI.
|
||||
|
||||
### Dados → forma (se aplicável)
|
||||
|
|
|
|||
|
|
@ -176,7 +176,10 @@ export const createApi = ({
|
|||
draft: Boolean(pr.draft),
|
||||
mergeable: pr.mergeable ?? null,
|
||||
autoMerge: pr.auto_merge
|
||||
? { mergeMethod: pr.auto_merge.merge_method ?? '', enabledBy: pr.auto_merge.enabled_by?.login ?? '' }
|
||||
? {
|
||||
mergeMethod: pr.auto_merge.merge_method ?? '',
|
||||
enabledBy: pr.auto_merge.enabled_by?.login ?? '',
|
||||
}
|
||||
: null,
|
||||
nodeId: pr.node_id ?? '',
|
||||
head: { ref: pr.head?.ref ?? '', sha: pr.head?.sha ?? '' },
|
||||
|
|
@ -417,18 +420,22 @@ export const createApi = ({
|
|||
* net arms auto-merge asynchronously AFTER the PR opens (the race is
|
||||
* real: a single disarm right after opening lost it in the OPS97
|
||||
* dogfood), so a bare DELETE/mutation is not enough — poll until
|
||||
* `auto_merge` reads null or attempts are exhausted. Resolves true
|
||||
* when verified disarmed; throws naming the PR otherwise.
|
||||
* `auto_merge` reads null or attempts are exhausted. Fail-closed: an
|
||||
* unknown PR throws (a wrong number must not read as success); the
|
||||
* final GET runs without a preceding sleep, so eventual consistency
|
||||
* may raise a false "still armed" — that direction is safe.
|
||||
*/
|
||||
ensureAutoMergeDisabled: async (number, { attempts = 5, delayMs = 2000 } = {}) => {
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
const pr = await api.getPullRequest(number)
|
||||
if (!pr || pr.autoMerge === null) return true
|
||||
if (!pr) throw new Error(`PR #${number} não encontrado — desarme não verificado`)
|
||||
if (pr.autoMerge === null) return true
|
||||
await api.disableAutoMerge(pr.nodeId)
|
||||
if (attempt < attempts) await sleepImpl(delayMs)
|
||||
}
|
||||
const final = await api.getPullRequest(number)
|
||||
if (!final || final.autoMerge === null) return true
|
||||
if (!final) throw new Error(`PR #${number} não encontrado — desarme não verificado`)
|
||||
if (final.autoMerge === null) return true
|
||||
throw new Error(
|
||||
`auto-merge do PR #${number} segue armado após ${attempts} tentativas de desarme`,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -141,11 +141,10 @@ describe('github-api (issue tracker layer)', () => {
|
|||
})
|
||||
const pr = await api.getPullRequest(5)
|
||||
expect(pr?.autoMerge?.mergeMethod).toBe('rebase')
|
||||
expect(pr?.head).toEqual({ ref: '', sha: '' })
|
||||
})
|
||||
|
||||
it('disableAutoMerge sends the GraphQL disable mutation with the node id', async () => {
|
||||
const calls: { url: string; init?: { method?: string; body?: string } }[] = []
|
||||
const calls: FetchCall[] = []
|
||||
const api = createApi({
|
||||
token: 'tok',
|
||||
fetchImpl: async (url, init) => {
|
||||
|
|
@ -161,6 +160,15 @@ describe('github-api (issue tracker layer)', () => {
|
|||
expect(payload.variables).toEqual({ id: 'PR_node7' })
|
||||
})
|
||||
|
||||
it('ensureAutoMergeDisabled throws fail-closed on an unknown PR', async () => {
|
||||
const api = createApi({
|
||||
token: 'tok',
|
||||
fetchImpl: async () => ok({ message: 'Not Found' }, 404),
|
||||
retries: 0,
|
||||
})
|
||||
await expect(api.ensureAutoMergeDisabled(13)).rejects.toThrow(/não encontrado/)
|
||||
})
|
||||
|
||||
it('ensureAutoMergeDisabled converges once a poll sees null', async () => {
|
||||
let poll = 0
|
||||
let disabledCalls = 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue