diff --git a/.cursor/rules/agent-pr-workflow.mdc b/.cursor/rules/agent-pr-workflow.mdc index b67e0da4..58e07e80 100644 --- a/.cursor/rules/agent-pr-workflow.mdc +++ b/.cursor/rules/agent-pr-workflow.mdc @@ -1,17 +1,12 @@ ---- -description: PR workflow for Teqo agents — overrides user-level gh-github draft defaults -alwaysApply: true ---- - # Agent PR workflow (Teqo) -**This repo overrides any user-level rule that says "always create PRs as draft".** Agent deliveries merge to `stage`, not `main`. +**This repo overrides any user-level rule that says "always create PRs as draft".** Agent deliveries merge to `main`. -## Create (Ready, base stage) +## Create (Ready, base main) ```bash pnpm push -u origin HEAD -gh pr create --base stage --title " — " --body "$(cat <<'EOF' +gh pr create --base main --title "<id> — <title>" --body "$(cat <<'EOF' ## O que é <2–4 linhas> @@ -21,7 +16,7 @@ EOF ``` - **Never** `--draft`. PRs must be **Ready for review** on creation. -- Base is always **`stage`** (never `main` — promote é automático via `promote-stage-to-main.yml` após CI stage green). +- Base is always **`main`**. ## Push (determinístico) @@ -31,12 +26,10 @@ EOF 2. `pnpm gate:push` — espelho do `ci-pr.yml` (inclui `format:check`) 3. `git push --no-verify` — só porque o gate já rodou no passo 2 (não é escape hatch) -Cursor Cloud já roda `pnpm install` em `.cursor/environment.json` (`install`); worktrees locais sem `pnpm i` dependem do passo 1. - ```bash -pnpm push -u origin HEAD # canonical -git push # só depois de `pnpm i` — hook repete ensure-deps + gate:push -git push --no-verify # WIP escape — CI repete tudo +pnpm push -u origin HEAD # canonical +git push # só depois de `pnpm i` — hook repete ensure-deps + gate:push +git push --no-verify # WIP / cutover escape — CI repete tudo; ver AGENT-OPS ``` ## Auto-merge immediately @@ -51,12 +44,12 @@ Do not wait for human approval. Auto-merge fires when **required** checks pass. ## Required checks vs Vercel -Branch protection on `stage` requires only: +Branch protection on `main` requires only: - `checks` (GitHub Actions rollup from `ci-pr.yml`) - `migration-lock` -**Vercel preview is NOT a merge gate.** Do not block on it. +**Vercel preview is NOT a merge gate.** Production deploy is Actions-gated (`ci.yml` → `vercel deploy --prod` after full suite green). Do not block on Vercel Git status. When watching CI: @@ -64,14 +57,12 @@ When watching CI: gh pr checks <PR> --watch --required ``` -Use `--required` — without it, `--watch` also waits for Vercel and can stall for minutes on the Hobby plan (one concurrent build). - -Once `checks` and `migration-lock` are green, auto-merge proceeds even if Vercel is still `PENDING`. +Use `--required` — without it, `--watch` also waits for Vercel. ## Agent branches skip Vercel builds -Branches `agent/*` and `cursor/*` use `scripts/vercel-ignore-build.sh` so parallel agents do not queue Vercel builds. Previews still deploy for `stage` and `main`. +All branches skip Vercel Git builds via `scripts/vercel-ignore-build.sh` (deploys only from the main verifier Action). -## Promote (human only) +## Promote -Never run `gh pr create --base main` or `pnpm agent:promote` in the agent flow. +There is no stage→main promote. Merging to `main` is the delivery; `ci.yml` verifies and deploys. diff --git a/.cursor/rules/engineering-standards.mdc b/.cursor/rules/engineering-standards.mdc index a7fddfce..6216047b 100644 --- a/.cursor/rules/engineering-standards.mdc +++ b/.cursor/rules/engineering-standards.mdc @@ -11,7 +11,7 @@ Established 2026-07-23 by the engineering-standards hardening (tracker: `docs/IM 1. **Dead code dies immediately.** After your edits, check what your change orphaned and delete it in the same delivery: run `pnpm exec knip` (files/dependencies are CI-blocking errors; unused exports are warnings being driven to zero). Never leave commented-out code, unused exports, superseded components, or one-shot scripts behind — git history is the archive. 2. **Zero warnings.** `pnpm lint` runs with `--max-warnings=0`. Deliberate placeholders use a `_` prefix. Never silence a rule without an inline justification comment. -3. **Gate em duas velocidades (desde 2026-07-30, paradigma de agentes paralelos — `docs/AGENT-OPS.md`).** **Fast gate local do agente** (iteração durante a sessão): `pnpm gate:fast` (lint + typecheck + test:unit). **Push canônico:** `pnpm push` — `ensure-repo-deps` (install + Husky em worktrees) + `git push`; o hook pre-push roda `gate:push` → `gate:ci` (espelho serial do `ci-pr.yml`, inclui `format:check`). Não use `git push` nu. Fonte única: scripts `gate:*` e `scripts/git-push.mjs` em `package.json`; escape para WIP: `git push --no-verify`. `migration-lock` só existe no GitHub (conta PRs abertos em schema). O handoff humano (promote `stage→main`) é automático após CI stage green. **CI vermelho no teu PR é teu, mesmo pré-existente ou de infra** — "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`). +3. **Gate em duas velocidades (desde 2026-07-30, paradigma de agentes paralelos — `docs/AGENT-OPS.md`).** **Fast gate local do agente** (iteração durante a sessão): `pnpm gate:fast` (lint + typecheck + test:unit). **Push canônico:** `pnpm push` — `ensure-repo-deps` (install + Husky em worktrees) + `git push`; o hook pre-push roda `gate:push` → `gate:ci` (espelho serial do `ci-pr.yml`, inclui `format:check`). Não use `git push` nu. Fonte única: scripts `gate:*` e `scripts/git-push.mjs` em `package.json`; escape para WIP: `git push --no-verify`. `migration-lock` só existe no GitHub (conta PRs abertos em schema). Entrega = merge em `main`; deploy gated pelo verificador `ci.yml`. **CI vermelho no teu PR é teu, mesmo pré-existente ou de infra** — "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`). 4. **Run gate commands bare, never piped** (`pnpm test | tail` swallows the exit code). Never run `knip --fix` blind — verify each removal with `git grep -w <symbol>` first (knip still cannot load `payload.config.ts`, so its graph is incomplete; ledgered as P3). ## Type honesty diff --git a/.cursor/skills/agent-pool/SKILL.md b/.cursor/skills/agent-pool/SKILL.md index 70209019..652b463b 100644 --- a/.cursor/skills/agent-pool/SKILL.md +++ b/.cursor/skills/agent-pool/SKILL.md @@ -7,7 +7,7 @@ description: Opera o pool de agentes Cursor Cloud do Teqo (supervisor remoto sta O pool mantém até **5 Cursor Cloud Agents** (configurável, máx 8) rodando `work-issue` sobre Issues `ready` elegíveis para autonomia. O supervisor é o workflow **`.github/workflows/agent-pool.yml`** (tick stateless a cada 10 min + a cada merge em `stage` + sob dispatch) rodando `scripts/agent-pool.mjs` — nenhum processo local, laptop fechado não importa. Arquitetura, elegibilidade (`isAutonomousClaimable`), anti-race (claim coordenado pelo supervisor) e ciclo de vida: `docs/plans/agent-pool-orchestrator.md`. -**O pool NUNCA faz `pnpm agent:promote`** — promote `stage→main` continua humano. +**O pool NUNCA deploya** — deploy gated fica em `ci.yml` após merge em `main`. ## Pré-requisitos (uma vez) @@ -31,7 +31,7 @@ Os wrappers disparam `gh workflow run agent-pool.yml -f action=…`; acompanhe c ## Ciclo de vida de uma Issue no pool -`ready` → (tick: claim coordenado, flip + marcador) → `in-progress` → worker spawnado com o `model:` da Issue (fallback `composer-2.5`) → PR `--base stage` com `Closes #N` → CI green → auto-merge → `done` (flip determinístico do CI) → tick arquiva o agente e repõe o slot. +`ready` → (tick: claim coordenado, flip + marcador) → `in-progress` → worker spawnado com o `model:` da Issue (fallback `composer-2.5`) → PR `--base main` com `Closes #N` → CI green → auto-merge → `done`+`in-prod` (flip determinístico do CI) → tick arquiva o agente e repõe o slot. - **Falha terminal** (run ERROR/CANCELLED/EXPIRED ou fim sem PR): tick comenta, move a Issue para `blocked` e arquiva o agente. **Triage humana**: ler o run em cursor.com/agents, decidir — re-`ready` manual se transitório (o circuit breaker recusa a 3ª tentativa automática), corrigir a spec se sistêmico. - **Worker travado**: archive em cursor.com/agents → o próximo tick reconcilia como falha documentada. diff --git a/.cursor/skills/work-issue/SKILL.md b/.cursor/skills/work-issue/SKILL.md index 0d89bb32..7884d7e1 100644 --- a/.cursor/skills/work-issue/SKILL.md +++ b/.cursor/skills/work-issue/SKILL.md @@ -1,13 +1,13 @@ --- name: work-issue -description: Conduz uma GitHub Issue rastreável do Teqo do claim ao merge em stage — prep (pnpm i), claim (pnpm agent:claim), renomear a sessão + abrir o plano no editor, verificação do modelo declarado na Issue, freshness audit do plano, execução das fases (schema/server → UI via /impeccable → gates), /simplify (3 reviewers paralelos via Task) + capture-review-debts, PR --base stage com Closes #N, acompanhamento do CI até o merge. Usar quando o usuário pedir para implementar/trabalhar uma Issue ("trabalha a issue #N", "implementa o B79", "pega a próxima issue", "vamos fazer o X", "continua a implementação"). +description: Conduz uma GitHub Issue rastreável do Teqo do claim ao merge em main — prep (pnpm i), claim (pnpm agent:claim), renomear a sessão + abrir o plano no editor, verificação do modelo declarado na Issue, freshness audit do plano, execução das fases (schema/server → UI via /impeccable → gates), /simplify (3 reviewers paralelos via Task) + capture-review-debts, PR --base main com Closes #N, acompanhamento do CI até o merge. Usar quando o usuário pedir para implementar/trabalhar uma Issue ("trabalha a issue #N", "implementa o B79", "pega a próxima issue", "vamos fazer o X", "continua a implementação"). --- -# Trabalhar uma Issue (claim → merge em stage) +# Trabalhar uma Issue (claim → merge em main) -Esta skill conduz UMA Issue rastreável do claim ao merge em `stage`. Substitui o antigo fluxo implement-roadmap-item + close-delivery + ship-to-main. **GitHub Issues são a fonte canônica** de spec/status/deps/prio/modelo; `docs/roadmap.md` é legado congelado e nunca é editado aqui (registro de entrega vai na Issue + `docs/plans/<slug>.md` + notebook do projeto). +Esta skill conduz UMA Issue rastreável do claim ao merge em `main`. Substitui o antigo fluxo implement-roadmap-item + close-delivery + ship-to-main. **GitHub Issues são a fonte canônica** de spec/status/deps/prio/modelo; `docs/roadmap.md` é legado congelado e nunca é editado aqui (registro de entrega vai na Issue + `docs/plans/<slug>.md` + notebook do projeto). -**Proibido neste fluxo:** qualquer acesso a DB de stage/prod, merge sem CI green, editar outras Issues `in-progress`. `pnpm agent:promote` é só override manual — o promote normal é automático após CI stage green. +**Proibido neste fluxo:** qualquer acesso a DB de prod, merge sem CI green, editar outras Issues `in-progress`. **Qualidade de decisão e dados:** aplique [decision-quality.md](../plan-issue/decision-quality.md) ao auditar e fatiar (caro vs barato, Opções+Recomendação+rejeitadas, appetite, rabbit holes, tracer bullet cedo) e [data-presentation.md](../plan-issue/data-presentation.md) ao auditar/fatiar superfícies com KPI/mapa/série/ranking. @@ -22,7 +22,7 @@ Esta skill conduz UMA Issue rastreável do claim ao merge em `stage`. Substitui - [ ] 3. Freshness audit (enxuto) do plano contra o repositório - [ ] 4. Executar as fases (schema/server → UI via /impeccable → gates de engenharia) - [ ] 5. /simplify no diff da sessão → capture-review-debts -- [ ] 6. Fechar em stage: branch → commit → **`pnpm push -u origin HEAD`** (não `git push` nu) → gh pr create --base stage (Closes #N, sem --draft) +- [ ] 6. Fechar em main: branch → commit → **`pnpm push -u origin HEAD`** (não `git push` nu) → gh pr create --base main (Closes #N, sem --draft) → gh pr merge --auto --merge → gh pr checks --watch --required até o merge → consolidar pontas soltas ``` @@ -145,15 +145,15 @@ Lance **três** subagentes via `Task` **em paralelo** na mesma mensagem — read 2. Achados maiores que o cleanup da sessão → skill `capture-review-debts` (registra via `agent:register`/`agent:file-miss`; **nunca** edita Issue `in-progress` — nem esta; débito do mesmo pai vira Issue nova com `depends: [<id-do-pai>]`). 3. Resuma o que corrigiu e o que ficou como recomendação/débito. -## Passo 6 — Fechar em stage +## Passo 6 — Fechar em main 1. Branch `agent/<id>-<slug>` (worktrees do Cursor são donos da criação; commits lógicos). 2. **`pnpm push -u origin HEAD`** — canonical; roda `ensure-repo-deps` + `gate:push` + push (não depende do Husky). Não use `git push` nu. -3. `gh pr create --base stage` com `Closes #<N>` no body — **nunca `--draft`** (`.cursor/rules/agent-pr-workflow.mdc`). -4. `gh pr merge --auto --merge <PR>` imediatamente após criar o PR (não precisa rebasear na head de `stage` se não houver conflito — `strict=false` na proteção de `stage`). -5. **Acompanhe só os checks obrigatórios** (`gh pr checks <PR> --watch --required`): gate = `checks` + `migration-lock`; Vercel não bloqueia. Falha no ci-pr → corrige na mesma branch. **Conflito de merge** → rebase em `stage` e reempurre; não force-push salvo para destravar CI após rebase. -6. O flip `in-progress → done` é **determinístico no CI** (workflow `issue-done-on-stage-merge.yml` lê o `Closes #N` no merge em stage). O agente espera o merge apenas para verificar e **consolidar pontas soltas**: débitos não registrados, doc da sessão pendente (notebook do projeto + `docs/plans/<slug>.md` com Status/Atualizado em), comentário de fechamento na Issue com o que entrou. Se o agente falhar depois do merge, o status já está correto. +3. `gh pr create --base main` com `Closes #<N>` no body — **nunca `--draft`** (`.cursor/rules/agent-pr-workflow.mdc`). +4. `gh pr merge --auto --merge <PR>` imediatamente após criar o PR (`strict=false` na proteção de `main`). +5. **Acompanhe só os checks obrigatórios** (`gh pr checks <PR> --watch --required`): gate = `checks` + `migration-lock`; Vercel Git não bloqueia. Falha no ci-pr → corrige na mesma branch. **Conflito de merge** → rebase em `main` e reempurre. +6. O flip `in-progress → done` + `in-prod` é **determinístico no CI** (`issue-done-on-main-merge.yml`). Deploy gated fica em `ci.yml` — não é passo do agente. Consolide pontas soltas após o merge. ## Resumo final ao usuário -Issue trabalhada + sessão renomeada + plano aberto no editor + verificação de modelo (declarado vs sessão, ou registro do ausente), veredito do freshness audit, o que entrou por fase, resultado do critique/polish (se UI), simplify (3 reviewers paralelos) + débitos registrados, gates (`pnpm push`, sem `--no-verify` cru), link do PR e estado do merge em stage, pontas soltas consolidadas. `in-prod` entra no auto-promote (`promote-stage-to-main.yml`) após CI stage green — não é passo do agente. +Issue trabalhada + sessão renomeada + plano aberto no editor + verificação de modelo (declarado vs sessão, ou registro do ausente), veredito do freshness audit, o que entrou por fase, resultado do critique/polish (se UI), simplify (3 reviewers paralelos) + débitos registrados, gates (`pnpm push`, sem `--no-verify` cru), link do PR e estado do merge em main, pontas soltas consolidadas. `done`+`in-prod` no merge; deploy gated em `ci.yml`. diff --git a/.github/workflows/agent-pool.yml b/.github/workflows/agent-pool.yml index 1a723256..8207894c 100644 --- a/.github/workflows/agent-pool.yml +++ b/.github/workflows/agent-pool.yml @@ -4,12 +4,13 @@ # POOL_MAX_SLOTS over `ready` autonomous-eligible issues. Three triggers: # - workflow_dispatch — the canonical human channel (start/stop/pause/resume) # - schedule */10min — the periodic reconcile (default branch only) -# - pull_request closed on stage — immediate slot replenish after a merge -# Schedule and PR triggers only run from the default branch: they activate -# after the human promote to main; until then dispatch on the branch works. +# - pull_request closed on main — immediate slot replenish after a merge # -# Secrets: CURSOR_API_KEY (repo secret) — Cursor Cloud Agents API key. -# GITHUB_TOKEN needs actions: write (pool state lives in repo variables). +# Secrets: +# CURSOR_API_KEY — Cursor Cloud Agents API key +# POOL_GITHUB_TOKEN — PAT with actions:write + issues:write (required to read/ +# write repo Actions variables; default GITHUB_TOKEN often 403s — mirror of +# the old PROMOTE_GITHUB_TOKEN pattern) name: Agent pool supervisor on: @@ -17,7 +18,7 @@ on: - cron: '*/10 * * * *' pull_request: types: [closed] - branches: [stage] + branches: [main] workflow_dispatch: inputs: action: @@ -36,7 +37,6 @@ on: required: false type: string -# Single-allocator invariant: ticks never overlap. concurrency: group: agent-pool cancel-in-progress: false @@ -51,7 +51,6 @@ jobs: tick: runs-on: ubuntu-latest timeout-minutes: 10 - # pull_request closed WITHOUT merge is not a replenish signal — skip. if: github.event_name != 'pull_request' || github.event.pull_request.merged == true steps: - uses: actions/checkout@v4 @@ -60,17 +59,14 @@ jobs: with: node-version: 24 cache: pnpm - # The pool scripts import scripts/lib/cli.mjs (dotenv) via agent-github — - # they are NOT dependency-free, so install like every sibling workflow. - run: pnpm install --frozen-lockfile - name: agent:pool workflow env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.POOL_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} ACTION: ${{ inputs.action }} MAX_SLOTS: ${{ inputs.maxSlots }} run: | - # schedule / pull_request merged → reconcile tick; dispatch → the chosen action. if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then args=(--action "$ACTION" --actor "$GITHUB_ACTOR") if [ -n "$MAX_SLOTS" ]; then args+=(--max-slots "$MAX_SLOTS"); fi diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index a91fac9c..25d5eeac 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -1,4 +1,4 @@ -# PR gate: feature → main (also stage until cutover). Two-phase cascade: +# PR gate: feature → main. Two-phase cascade: # Phase 1 (cheap, no Postgres): lint, format, typecheck, knip, cycles, unit # Phase 2 (expensive, Postgres; needs phase 1 green/skipped): int, build, e2e # Job-level skips come from the `scope` classifier (scripts/ci-scope.mjs). A @@ -12,7 +12,7 @@ name: CI (PR) on: pull_request: - branches: [stage, main] + branches: [main] concurrency: group: ci-pr-${{ github.event.pull_request.number }} diff --git a/.github/workflows/ci-stage.yml b/.github/workflows/ci-stage.yml deleted file mode 100644 index d2bb33ba..00000000 --- a/.github/workflows/ci-stage.yml +++ /dev/null @@ -1,77 +0,0 @@ -# Stage gate: runs AFTER a merge lands on `stage`, against the stage database -# (Neon branch cloned from prod, secret STAGE_DATABASE_URL in the `stage` -# GitHub Environment). -# -# HARD RULE: never `pnpm build` here — the build step runs `payload migrate` -# against whatever DATABASE_URL it sees, and this workflow holds a stage URL. -# No db:seed:minimal either: stage data is the prod snapshot, not the synthetic -# minimal set. -name: CI (stage) - -on: - push: - branches: [stage] - -concurrency: - group: ci-stage - cancel-in-progress: false - -jobs: - stage-checks: - runs-on: ubuntu-latest - # The int suite against a remote Neon snapshot is latency-bound (~120 ms - # RTT per query): the FULL suite blew the 30-minute timeout twice on - # 2026-07-30, so stage runs a curated smoke subset (access lockdown + - # consent-gated flows — the things a prod-snapshot run is FOR). The full - # int suite gates in ci-pr.yml against the local service DB. Subset rule: - # only specs safe against REAL data — own-fixture creates are fine; specs - # that delete/mutate seeded rows are not (submitWhatsapp deletes the keyed - # Consent to test fail-closed, and campaignMigrationReconciliation creates - # scratch databases; both stay out). - timeout-minutes: 45 - environment: stage - env: - DATABASE_URL: ${{ secrets.STAGE_DATABASE_URL }} - PAYLOAD_SECRET: ${{ secrets.STAGE_PAYLOAD_SECRET }} - NEXT_PUBLIC_SITE_URL: https://teqo-stage.vercel.app - # The seed/migrate guards refuse non-local hosts without this flag. Stage - # is the ONE documented remote target, and only this workflow may use it. - ALLOW_REMOTE_DB: 'true' - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Apply pending migrations to the stage database - run: pnpm migrate - - - name: Point the test env at the stage snapshot - run: | - { - echo "DATABASE_URL=${DATABASE_URL}" - echo "PAYLOAD_SECRET=${PAYLOAD_SECRET}" - } > .env.test - - - name: Integration smoke tests against the stage snapshot - run: > - pnpm test:int - --testTimeout=120000 --hookTimeout=120000 - tests/int/collectionAccessLockdown.int.spec.ts - tests/int/campaignMunicipalityAccess.int.spec.ts - tests/int/campaignVotePledge.int.spec.ts - tests/int/campaignSupporter.int.spec.ts - tests/int/campaignDemandWorkflow.int.spec.ts - tests/int/onda0Provision.int.spec.ts - env: - # Documented escape hatch in tests/helpers/assertTestDatabase.ts — - # only this workflow may set it (vitest.setup.ts loads .env.test - # with override: true, written by the step above). - ALLOW_STAGE_TEST_DB: 'true' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ca22099..c1721270 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,23 +1,19 @@ -# Continuous integration on GitHub Actions (GitHub is the only home). -# Post-merge gate on `main` — the FULL safety net, always: the promote PR -# already passed ci-pr (possibly with affected-test selection), so this run -# re-verifies everything against the minimal synthetic database, including -# build and full e2e. -# PRs run in ci-pr.yml; merges to `stage` run in ci-stage.yml (stage Neon -# snapshot smoke — never a build there). +# Post-merge verifier on `main`: always the FULL suite (no affected skips). +# When green, deploys production via `vercel deploy --prod` (Vercel Git builds +# are ignored — see scripts/vercel-ignore-build.sh). If HEAD moved while this +# run was in flight, `requeue` dispatches another run for the new tip. name: CI on: push: branches: [main] + workflow_dispatch: concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true + group: main-verify + cancel-in-progress: false env: - # Tests load .env.test themselves (database teqo_test on localhost:5432). - # These are only for the migrate/seed/build steps, which read the process env. DATABASE_URL: postgresql://teqo:teqo@localhost:5432/teqo_test PAYLOAD_SECRET: test-only-secret-not-used-in-production NEXT_PUBLIC_SITE_URL: http://localhost:3000 @@ -34,6 +30,7 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile + - run: node scripts/check-test-locations.mjs - run: pnpm lint - run: pnpm format:check - run: pnpm typecheck @@ -46,7 +43,6 @@ jobs: timeout-minutes: 25 services: postgres: - # Matches production Neon major (17) and docker-compose.yml. image: postgres:17-alpine env: POSTGRES_USER: teqo @@ -96,7 +92,6 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - - name: Build (against the local service database) run: pnpm build @@ -125,29 +120,24 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - - name: Install Playwright chromium run: pnpm exec playwright install --with-deps chromium - - run: pnpm migrate - run: pnpm db:seed:minimal - - name: Build for e2e run: pnpm build env: NEXT_DIST_DIR: .next/e2e - - name: E2E tests (full suite) run: pnpm test:e2e - # Same legacy branch-protection context as ci-pr.yml (see comment there). checks: name: checks runs-on: ubuntu-latest needs: [static, int, build, e2e] if: always() steps: - - name: All parallel checks green + - name: All main verifier checks green env: NEEDS: ${{ toJSON(needs) }} run: | @@ -156,3 +146,78 @@ jobs: to_entries | all(.value.result == "success" or .value.result == "skipped") ' + + deploy: + runs-on: ubuntu-latest + needs: [checks] + if: needs.checks.result == 'success' + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Guard stale HEAD + id: tip + run: | + set -euo pipefail + git fetch origin main --quiet + tip=$(git rev-parse origin/main) + if [ "$tip" != "$GITHUB_SHA" ]; then + echo "Skip deploy: origin/main is $tip, this run verified $GITHUB_SHA" + echo "stale=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "stale=false" >> "$GITHUB_OUTPUT" + - name: Require Vercel credentials + if: steps.tip.outputs.stale != 'true' + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + set -euo pipefail + missing=() + [ -z "${VERCEL_TOKEN:-}" ] && missing+=(VERCEL_TOKEN) + [ -z "${VERCEL_ORG_ID:-}" ] && missing+=(VERCEL_ORG_ID) + [ -z "${VERCEL_PROJECT_ID:-}" ] && missing+=(VERCEL_PROJECT_ID) + if [ "${#missing[@]}" -gt 0 ]; then + echo "::error::Missing repo secrets for gated deploy: ${missing[*]}. Add them under Settings → Secrets, then re-run this workflow." + exit 1 + fi + - name: Deploy production + if: steps.tip.outputs.stale != 'true' + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + set -euo pipefail + npx --yes vercel@41 pull --yes --environment=production --token="$VERCEL_TOKEN" + npx --yes vercel@41 build --prod --token="$VERCEL_TOKEN" + npx --yes vercel@41 deploy --prebuilt --prod --token="$VERCEL_TOKEN" + + requeue: + runs-on: ubuntu-latest + needs: [checks, deploy] + if: always() + permissions: + actions: write + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Re-dispatch if main advanced + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + git fetch origin main --quiet + tip=$(git rev-parse origin/main) + if [ "$tip" = "$GITHUB_SHA" ]; then + echo "HEAD still $GITHUB_SHA — idle" + exit 0 + fi + echo "origin/main moved to $tip (verified $GITHUB_SHA) — requeue ci.yml" + gh workflow run ci.yml --ref main diff --git a/.github/workflows/issue-done-on-stage-merge.yml b/.github/workflows/issue-done-on-main-merge.yml similarity index 60% rename from .github/workflows/issue-done-on-stage-merge.yml rename to .github/workflows/issue-done-on-main-merge.yml index 2fe239cd..ecba387a 100644 --- a/.github/workflows/issue-done-on-stage-merge.yml +++ b/.github/workflows/issue-done-on-main-merge.yml @@ -1,4 +1,4 @@ -name: Stage merge flips claimed issues to done +name: Main merge flips claimed issues to done + in-prod on: pull_request: @@ -11,18 +11,18 @@ permissions: jobs: issue-done: - if: github.event.pull_request.base.ref == 'stage' && github.event.pull_request.merged == true + if: github.event.pull_request.base.ref == 'main' && github.event.pull_request.merged == true runs-on: ubuntu-latest env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} steps: - - name: Flip Closes/Fixes #N issues from in-progress to done + - name: Flip Closes/Fixes #N issues to done and in-prod run: | body="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json body --jq .body)" numbers="$(printf '%s' "$body" | grep -oiE '(closes|fixes) #[0-9]+' | grep -oE '[0-9]+' | sort -u)" for n in $numbers; do - gh issue edit "$n" --repo "$GITHUB_REPOSITORY" --remove-label in-progress --add-label done || true + gh issue edit "$n" --repo "$GITHUB_REPOSITORY" --remove-label in-progress --add-label done --add-label in-prod || true gh issue comment "$n" --repo "$GITHUB_REPOSITORY" \ - --body "Merged em stage via PR #${PR_NUMBER} — done; \`in-prod\` entra no auto-promote após CI stage green." || true + --body "Merged em main via PR #${PR_NUMBER} — \`done\` + \`in-prod\`. Deploy de produção segue o verificador \`ci.yml\` (gated)." || true done diff --git a/.github/workflows/promote-stage-to-main.yml b/.github/workflows/promote-stage-to-main.yml deleted file mode 100644 index 39e1ca77..00000000 --- a/.github/workflows/promote-stage-to-main.yml +++ /dev/null @@ -1,94 +0,0 @@ -# Auto-promote: when ci-stage is green on the current `stage` head, merge stage → main. -# Replaces the manual `pnpm agent:promote --i-am-human` gate. The human review on main -# is replaced by ci-stage (Neon smoke) + ci.yml on main (full safety net post-merge). -# -# Stale runs are ignored: only the workflow run whose headSha still matches -# origin/stage may promote (newest commit wins when ci-stage queues). -name: Promote stage to main - -on: - workflow_run: - workflows: ['CI (stage)'] - types: [completed] - branches: [stage] - -concurrency: - group: promote-stage-to-main - cancel-in-progress: false - -permissions: - contents: write - pull-requests: write - -jobs: - promote: - if: github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - # Admin PAT bypasses main branch protection (required review + status checks on - # the promote PR). Falls back to GITHUB_TOKEN when the PAT is not configured. - GH_TOKEN: ${{ secrets.PROMOTE_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - RUN_SHA: ${{ github.event.workflow_run.head_sha }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Promote only when this run is still the stage head - id: gate - run: | - set -euo pipefail - git fetch origin main stage - stage_sha="$(git rev-parse origin/stage)" - main_sha="$(git rev-parse origin/main)" - - if [ "$RUN_SHA" != "$stage_sha" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Stale ci-stage run ($RUN_SHA); current stage head is $stage_sha — skip." - exit 0 - fi - - if git merge-base --is-ancestor origin/main origin/stage; then - if [ "$main_sha" = "$stage_sha" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "main already at stage head ($stage_sha) — nothing to promote." - exit 0 - fi - else - echo "::error::main diverged from stage (hotfix on main?). Merge main into stage, wait for ci-stage green, then re-run." - exit 1 - fi - - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "Promoting stage $stage_sha → main ($main_sha)." - - - name: Merge stage into main - if: steps.gate.outputs.skip != 'true' - run: | - set -euo pipefail - pr="$(gh pr list --state open --base main --head stage --json number --jq '.[0].number // empty')" - if [ -z "$pr" ]; then - gh pr create \ - --base main \ - --head stage \ - --title "Promote stage → main (auto)" \ - --body "Auto-promote após CI (stage) verde. Gate: ci-stage no head de stage. Rede pós-merge: ci.yml em main." - pr="$(gh pr list --state open --base main --head stage --json number --jq '.[0].number')" - fi - - # --admin bypasses the human review + pre-merge status checks on main; ci-stage - # is the intentional pre-promote gate (see docs/AGENT-OPS.md). - gh pr merge "$pr" --admin --merge - echo "Promoted via PR #$pr." - - - name: Label promoted issues in-prod - if: steps.gate.outputs.skip != 'true' - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - for n in $(gh issue list --state open --label done --limit 200 --json number --jq '.[].number'); do - gh issue edit "$n" --add-label in-prod --remove-label done || true - done diff --git a/AGENTS.md b/AGENTS.md index 37eacf78..0854098c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public si ## Histórico de entregas (movido) -"Known Gaps" e os blocos "Recently resolved" saíram deste arquivo em 2026-07-30: vivem em [`docs/CHANGELOG-AGENTS.md`](docs/CHANGELOG-AGENTS.md). Novas entregas registram UMA entrada curta lá, não aqui. A referência genérica de Payload (config/collections/fields/hooks/queries/components) foi para [`docs/PAYLOAD-REFERENCE.md`](docs/PAYLOAD-REFERENCE.md) — consulta sob demanda, não always-on. Operação do paradigma de agentes paralelos (claim→PR→stage→promote humano): [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md). +"Known Gaps" e os blocos "Recently resolved" saíram deste arquivo em 2026-07-30: vivem em [`docs/CHANGELOG-AGENTS.md`](docs/CHANGELOG-AGENTS.md). Novas entregas registram UMA entrada curta lá, não aqui. A referência genérica de Payload (config/collections/fields/hooks/queries/components) foi para [`docs/PAYLOAD-REFERENCE.md`](docs/PAYLOAD-REFERENCE.md) — consulta sob demanda, não always-on. Operação do paradigma de agentes paralelos (claim→PR→main→deploy gated): [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md). ## Posts & Tags (news / publications) diff --git a/README.md b/README.md index 5a26e74e..125f9616 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,15 @@ > **Cheatsheet de operação agentic (humanos)** — o fluxo em 5 linhas: > > 1. O agente roda `pnpm agent:claim` e pega a próxima Issue `ready` por prioridade (`prio:P0..P3`). -> 2. Ele implementa, roda o fast gate (`lint + typecheck + unit`) e abre **PR para `stage`** — sozinho, e para aí. -> 3. CI verde → o PR mergeia em `stage` automaticamente; o merge roda o CI de stage contra o clone Neon de prod. -> 4. **Você** promove quando quiser: `pnpm agent:promote -- --i-am-human` (PR `stage→main`, merge só com CI stage + PR green). -> 5. Semanalmente: `pnpm db:refresh:stage` renova o snapshot do stage a partir de prod (humano, requer `NEON_API_KEY`). +> 2. Ele implementa, roda o fast gate (`lint + typecheck + unit`) e abre **PR para `main`** — sozinho. +> 3. CI verde → o PR mergeia em `main` automaticamente; o verificador `ci.yml` roda a suíte completa. +> 4. Suíte verde → Action dispara `vercel deploy --prod` (Vercel Git builds estão desligados). +> 5. Secrets humanos (uma vez): `VERCEL_*`, `POOL_GITHUB_TOKEN`; `pnpm configure:branch-protection`. > -> Comandos: `pnpm agent:claim | agent:register | agent:prioritize | agent:file-miss | agent:promote` e `pnpm db:seed:minimal | db:refresh:stage`. +> Comandos: `pnpm agent:claim | agent:register | agent:prioritize | agent:file-miss | agent:pool` e `pnpm db:seed:minimal`. > Labels: estado `ready|in-progress|blocked|done|in-prod`, `prio:P0..P3`, `kind:*`, `needs:migration|consent`, `requirements-changed`. -> **Agente faz sozinho:** claim → implementar → PR → stage. **Só humano:** promote `stage→main`, refresh do stage, envs Vercel/Neon, `pnpm build` contra banco remoto. -> Tudo em detalhe: [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md) · CI: `.github/workflows/ci-pr.yml` + `ci-stage.yml` + `ci.yml`. +> **Agente faz sozinho:** claim → implementar → PR → main. **Só humano:** secrets Vercel/pool, branch protection, envs Neon. +> Tudo em detalhe: [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md) · CI: `.github/workflows/ci-pr.yml` + `ci.yml`. Teqo starts as the official digital platform for **deputado Jorge Solla** and evolves into a **white-label civic engagement platform** for politicians in Brazil. diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index 0c4c874b..3c685e82 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -1,105 +1,89 @@ # AGENT-OPS — operação do paradigma de agentes paralelos -Uma página. Norte: simplicidade (só GitHub + Cursor), agente para no merge a `stage`, `main`/prod via auto-promote quando o CI stage fica verde. +Uma página. Norte: simplicidade (só GitHub + Cursor), agente entrega em `main`, deploy de produção só após o verificador full-suite verde. ## 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 de PR | -| **Stage** (Neon branch `stage`, clone de prod) | snapshot 100% de prod (PII!) | só CI `ci-stage.yml` + humanos em smoke controlado | -| **Prod** (Neon prod) | real | deploy Vercel a partir de `main` apenas | +| 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 | +| **Prod** (Neon prod) | real | deploy Vercel gated por `ci.yml` | -Agentes **nunca** recebem `DATABASE_URL` de stage/prod e nunca setam `ALLOW_REMOTE_DB`. O escape `ALLOW_STAGE_TEST_DB` (`tests/helpers/assertTestDatabase.ts`) só existe para o workflow `ci-stage.yml`. +Agentes **nunca** recebem `DATABASE_URL` de prod e nunca setam `ALLOW_REMOTE_DB`. Não há mais smoke Neon `stage` / `ALLOW_STAGE_TEST_DB` / `ci-stage.yml`. ## Fluxo ```text -claim → feature branch → PR --base stage → CI PR green → auto-merge em stage (sem exigir estar na head de stage; conflito = rebase) -stage → CI stage green → auto-promote (workflow promote-stage-to-main.yml) → main → Vercel prod +claim → feature branch → PR --base main → CI PR green (cascade + skips) → auto-merge em main +main → ci.yml full suite → vercel deploy --prod (se verde) → requeue se HEAD andou ``` -**Skills do fluxo (desde 2026-07-30):** `plan-issue` (ideia → plano completo + gate de confirmação + Issue via `agent:register --model`) → `work-issue` (claim → verificação de modelo → freshness audit → execução → /simplify → PR `--base stage` → acompanha CI até o merge) → `project-status` (overview/fila/mermaid/bloqueios/consolidação, read-only). `docs/roadmap.md` = **legado congelado** (stub); a fonte canônica são as GitHub Issues. +**Skills:** `plan-issue` → `work-issue` (claim → modelo → freshness → execução → /simplify → PR `--base main` → CI até o merge) → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = GitHub Issues. -- **Agente faz sozinho:** `pnpm agent:claim` → implementa → **`pnpm push`** (bootstrap deps/hooks + hook `gate:push`) → `gh pr create --base stage` com `Closes #N` (**Ready**, sem `--draft`) → `gh pr merge --auto --merge` → `gh pr checks --watch --required` até o merge. Regra PR: `.cursor/rules/agent-pr-workflow.mdc`. +- **Agente faz sozinho:** `pnpm agent:claim` → implementa → **`pnpm push`** → `gh pr create --base main` com `Closes #N` (**Ready**) → `gh pr merge --auto --merge` → `gh pr checks --watch --required`. Regra: `.cursor/rules/agent-pr-workflow.mdc`. +- **Só humano:** secrets Vercel (`VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID`), `POOL_GITHUB_TOKEN`, `pnpm configure:branch-protection`, editar envs Neon/Vercel. -### Dono do PR, dono do CI (desde 2026-07-30 — incidente PR #50) +### Dono do PR, dono do CI -**Done = PR mergeado em `stage` com CI verde.** Falha de CI no teu PR — infra do workflow, teste pré-existente ou regressão da feature — é tua até o merge. "Fora do escopo da feature" não é motivo para parar: corrige na mesma branch. Triagem das três classes: (a) **infra do workflow** (job mal configurado, step faltando) → fix no workflow no mesmo PR; (b) **teste pré-existente quebrado ou frágil** → repair no mesmo PR, no padrão dos specs irmãos; (c) **regressão da feature** → fix normal. Exceção de blast radius: se o fix exigir migration, access control ou Consent, para e escala com Opções — esses nunca se corrigem "de passagem". Flaky não se resolve com retry: repair, ou quarentena justificada no PR + débito registrado (`capture-review-debts`). +**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. -- **Só humano:** `pnpm db:refresh:stage` (refresh semanal do snapshot), editar envs Vercel/Neon, rodar `pnpm build` contra qualquer banco remoto. `pnpm agent:promote --i-am-human` permanece como override manual (promove o último commit ci-stage green em `stage`, mesmo com head vermelho). -- **Auto-promote:** `promote-stage-to-main.yml` dispara quando `ci-stage.yml` termina com sucesso. Só promove se o `headSha` do run ainda for o head de `stage` (commit mais novo ganha quando o CI enfileira). Recusa se `main` divergiu de `stage` (hotfix: merge `main` em `stage` + CI stage green de novo). -- **Merge em stage sem estar na head:** branch protection de `stage` com `strict=false` — PR pode auto-merger com CI green mesmo que outro PR tenha entrado antes; conflito de merge continua bloqueando (rebase obrigatório). Aplicar/reparar: `pnpm configure:branch-protection`. -- **Secret opcional:** `PROMOTE_GITHUB_TOKEN` (PAT admin do repo) se `GITHUB_TOKEN` não conseguir `gh pr merge --admin` na proteção de `main`. Settings → Actions → "Allow GitHub Actions to create and approve pull requests" também ajuda. -- **Engineering-audit é solitário:** quando roda, o paralelismo pausa (nenhum outro agente no repo) e o agente do audit executa as remediações P0/P1 na mesma sessão; inclui o harvest `kind:agent-miss` → guardrails. +### 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 `checks` + `migration-lock` 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 no stdout. Deps sem Issue = itens entregues do roadmap (satisfeitas, avisadas no brief) | -| `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue (spec + labels + frontmatter com `model:` quando informado) | -| `pnpm agent:status` | Read-only: overview, fila na ordem do claim (com `model:`), mermaid, bloqueios, sugestões de consolidação | -| `pnpm agent:prioritize -- <issue> <P0..P3>` | Troca a label `prio:*` | -| `pnpm agent:file-miss -- --title ...` | Issue `kind:agent-miss` → harvest em guardrail (`docs/GUARDRAILS.md`) | -| `pnpm agent:pool -- status\|tick --dry-run\|doctor` | Inspeção local do pool de workers Cloud; start/stop/pause **remotos** via `gh workflow run agent-pool.yml -f action=…` (ver "Agent pool" abaixo) | -| `pnpm agent:promote -- --i-am-human` | **Override manual:** PR `main` ← último commit em `stage` com ci-stage green (não exige head verde; auto-promote só no head) | -| `pnpm push [-- -u origin HEAD …]` | **Canônico:** `ensure-repo-deps` + `gate:push` + `git push --no-verify` (gate no script — não depende do Husky) | -| `pnpm configure:branch-protection [-- --dry-run]` | Ajusta proteção de `stage` (`strict=false`) e `main` (0 reviews — auto-promote substitui o humano) | -| `pnpm db:seed:minimal` | DB mínimo sintético (contrato: [`scripts/lib/seed-minimal-manifest.mjs`](../scripts/lib/seed-minimal-manifest.mjs), pin `tests/unit/seedMinimalManifest.unit.spec.ts`) | -| `pnpm db:refresh:stage` | **Humano:** nova Neon branch `stage` de prod + swap do secret `STAGE_DATABASE_URL` (requer `NEON_API_KEY`) | +| Comando | Faz | +| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| `pnpm agent:claim [-- --dry-run]` | Fila ready+unblocked por prio → `in-progress` + brief | +| `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue | +| `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; start/stop via `gh workflow run agent-pool.yml` | +| `pnpm push [-- -u origin HEAD …]` | Canônico: ensure-deps + gate:push + push | +| `pnpm configure:branch-protection [-- --dry-run]` | Proteção de `main`: `checks` + `migration-lock`, `strict=false`, 0 reviews | +| `pnpm db:seed:minimal` | DB mínimo sintético | -Labels: estado `ready|in-progress|blocked|done|in-prod`, `prio:P0..P3`, `kind:feature|defect|chore|agent-miss`, `needs:migration|consent`, `requirements-changed`. Issues carregam frontmatter `id/depends/serializes/priority/model` no body. +`pnpm agent:promote` e `pnpm db:refresh:stage` são **legado** (recusam / não fazem parte do fluxo). Labels: `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, `needs:*`. -## Contrato de PR (toda entrega) +## Contrato de PR -1. Se toca schema (migration, collection, Consent key, dado de boot) → **atualizar `db:seed:minimal` no mesmo PR** (label `needs:migration`). CI PR falha se `migrate + seed:minimal + test:int` não passam. -2. ≤1 PR aberto tocando `src/migrations/` | `payload-types.ts` (job `migration-lock`). -3. `docs/plans/<slug>.md` novo só para Issue nova — nunca recriar plans históricos no merge. -4. **Ready + auto-merge:** ver `.cursor/rules/agent-pr-workflow.mdc` — `gh pr create --base stage` (sem `--draft`) → `gh pr merge --auto --merge`; acompanhar com `gh pr checks --watch --required` (Vercel não é gate). +1. Schema → atualizar `db:seed:minimal` no mesmo PR (`needs:migration`). +2. ≤1 PR aberto tocando migrations / `payload-types.ts` (`migration-lock`). +3. Ready + auto-merge: `gh pr create --base main` → `gh pr merge --auto --merge`; `gh pr checks --watch --required` (Vercel Git não é gate). ## CI por alvo -| Workflow | Trigger | Banco | Passos | -| --------------------------- | --------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ci-pr.yml` | PR → `stage`/`main` | service Postgres 17 (mínimo) | jobs paralelos: `lint`/`format`/`typecheck`/`knip`/`cycles` · `unit`/`int` (**afetados pelo diff**: `vitest --changed` vs base do PR; fallback full em paths de risco — `scripts/test-affected.mjs`) · `e2e` (manifesto path→spec — `scripts/e2e-affected.mjs`, bloqueante) · `build` + `migration-lock`; job rollup **`checks`** agrega todos (nome legado exigido pela branch protection — não re-serializa a suíte) | -| `ci-stage.yml` | push em `stage` | `STAGE_DATABASE_URL` (Environment `stage`) | **smoke** `migrate` + subset int curado contra snapshot Neon (não é suíte completa — full int estourou timeout 2× em 2026-07-30). Gate de **promote automático** para `main`. **NUNCA `pnpm build`** | -| `promote-stage-to-main.yml` | ci-stage success em `stage` | — | auto-merge `stage→main` se head ainda é o de `stage`; marca Issues `done` → `in-prod` | -| `ci.yml` | push em `main` | service Postgres | **gate completo sempre** (rede de segurança pós-promote): estático + unit + int full (com seed:minimal) + build + e2e full | +| Workflow | Trigger | Banco | Passos | +| ------------------------------ | -------------------------------------- | ------------------- | ------------------------------------------------------------------------- | +| `ci-pr.yml` | PR → `main` | service Postgres 17 | Fase 1 barata → fase 2 cara; skips via `ci-scope.mjs`; rollup `checks` | +| `ci.yml` | push / dispatch `main` | service Postgres | Full sempre → `deploy` (`vercel deploy --prod`) → `requeue` se HEAD andou | +| `issue-done-on-main-merge.yml` | PR merged → `main` | — | `in-progress` → `done` + `in-prod` | +| `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) | -Fast gate local do agente (iteração): `pnpm gate:fast` (lint + typecheck + test:unit). **Push canônico:** `pnpm push` — `ensure-repo-deps` + `gate:push` no script + push (`--no-verify` só porque o gate já rodou). Hook `.husky/pre-push` repete o mesmo para quem usa `git push` após `pnpm i`. Cursor Cloud: `pnpm install` já está em `.cursor/environment.json` (`install`). Requer Postgres local com `teqo_test`. Escape WIP: `git push --no-verify` sem `pnpm push`. Debug e2e: `pnpm test:e2e:affected`. +Fast gate: `pnpm gate:fast`. Push: `pnpm push`. -## Stage DB — runbook de refresh (semanal, humano) +### Secrets (humano, uma vez) -1. `export NEON_API_KEY=…` (console.neon.tech → API keys; projeto Neon da campanha (id via `NEON_PROJECT_ID`; não hardcodar no repo público)). -2. `pnpm db:refresh:stage` — deleta a branch `stage` velha, cria nova do snapshot de prod, faz swap do secret `STAGE_DATABASE_URL` no Environment `stage`. `--dry-run` inspeciona. -3. Próximo `ci-stage` já usa o snapshot novo. Preview Vercel da branch `stage` (`jorgesolla-git-stage-solla.vercel.app`) aponta para o mesmo banco via envs `Preview (stage)` — **atenção**: o endpoint muda a cada refresh; rode também `vercel env add DATABASE_URL preview stage` com a URL nova (o script imprime a URL mascarada; pegue a completa no console Neon ou rode `vercel env pull`). +| Secret | Uso | +| ------------------- | ------------------------------------------------------------------------------------ | +| `VERCEL_TOKEN` | Deploy gated em `ci.yml` | +| `VERCEL_ORG_ID` | idem | +| `VERCEL_PROJECT_ID` | idem | +| `POOL_GITHUB_TOKEN` | PAT `actions:write` + `issues:write` (variables do pool; `GITHUB_TOKEN` costuma 403) | + +Vercel Git builds: `scripts/vercel-ignore-build.sh` skipeia **todas** as branches (incl. `main`) — prod só via Action. ## Cursor Cloud -`.cursor/environment.json` instala deps, sobe Postgres nativo (`ensure-postgres.sh` — VMs Cloud não rodam Docker/compose), migra e roda `db:seed:minimal` — clone + `gh` + DB mínimo bastam. Sem secrets de stage/prod. Hooks do repo são portáteis (paths relativos em `.cursor/hooks.json`); hooks pessoais (ex.: impeccable) ficam no `~/.cursor/hooks.json` do usuário. +`.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. -**Agentes Cloud (skill `work-issue`):** renomear sessão após claim (`rename_chat:` no brief); **não** usar `pnpm db:start` (Docker); **não** `git push --no-verify` cru — usar `pnpm push` (roda `gate:push` antes). `gate:fast`/`gate:push` não precisam de Postgres (`test:unit` não conecta); int/migrate usam o Postgres nativo do install. +## Agent pool -## Agent pool (orquestrador de workers Cloud) +Supervisor remoto (`agent-pool.yml`): schedule + replenish em merge → `main` + dispatch. Workers PR `--base main`. Default model `composer-2.5`. **Secret `POOL_GITHUB_TOKEN`** obrigatório se `GITHUB_TOKEN` 403 em `/actions/variables`. Doctor: `pnpm agent:pool -- doctor`. Skill: `agent-pool`. -Supervisor **remoto** que mantém até 5 Cursor Cloud Agents rodando `work-issue` sobre Issues `ready` elegíveis para autonomia (label `ready`, sem `blocked`/`requirements-changed`/`needs:consent`, deps satisfeitas; `needs:migration` cede ao `migration-lock`). Implementação: `.github/workflows/agent-pool.yml` (tick stateless: `schedule */10min` + `pull_request` closed em `stage` + `workflow_dispatch`) rodando `scripts/agent-pool.mjs`; workers spawnados via Cloud Agents API v1 com o `model:` da Issue (fallback `composer-2.5`). Canal canônico humano: `gh workflow run agent-pool.yml -f action=start|stop|pause|resume` (wrappers: `pnpm agent:pool -- start|stop|pause|resume|status|doctor`). Estado escalar em repo variables `POOL_*`; ativos derivados (marcadores `pool-worker` nos comentários + status dos runs). Claim coordenado pelo supervisor (alocador único, sem race); falha terminal → Issue a `blocked` para triagem; fila drenada → pool se desliga. **Nunca** chama `agent:promote`. Requer repo secret `CURSOR_API_KEY`; `schedule`/trigger de PR só ativam após merge em `main`. Arquitetura e runbook: [`docs/plans/agent-pool-orchestrator.md`](plans/agent-pool-orchestrator.md) + skill `agent-pool`. +## Guardrails / leitura -## Guardrails progressivos - -`pnpm agent:file-miss` → Issue `kind:agent-miss` → harvest periódico → guardrail programático (spec `codebaseConventions`, ESLint, check CI) → ledger `docs/GUARDRAILS.md`. Padrão já fichado: "migration sem atualizar seed:minimal". - -## Leitura always-on (ordem) - -`AGENTS.md` (fatiado 2026-07-30) → `.cursor/rules/codebase-map.mdc` → `.cursor/rules/engineering-standards.mdc` → esta página → kernels [`PRODUCT.md`](../PRODUCT.md) / [`DESIGN.md`](../DESIGN.md) / [`CUSTOMER.md`](CUSTOMER.md) (kernel no topo). Histórico: [`CHANGELOG-AGENTS.md`](CHANGELOG-AGENTS.md). Referência Payload sob demanda: [`PAYLOAD-REFERENCE.md`](PAYLOAD-REFERENCE.md). - -## Material sensível: `/private/` (local) vs cloud - -O repo GitHub é **público** e proprietário (All Rights Reserved). Discovery de campo com áudio, transcrição bruta, nomes reais, deltas operacionais de voto e planilhas de projeção **não** sobem no tip público. - -| Quem | Onde grava | O que faz | -| -------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Agente local** | `/private/` (gitignored; só `private/README.md` versionado) | Lê/escreve notas sensíveis aí. Publica em `docs/` só uma **versão limpa** (JTBD/insights sem PII operacional). Nunca `git add` de áudio/transcribe/sheets/notas verbatim. | -| **Agente cloud** (Cursor Cloud, Devin, etc.) | Clone **sem** `/private/` | Baseia-se em `docs/CUSTOMER.md` e planos públicos limpos. Se o entregável for sensível, **informa no chat** e **não commitá** — o humano salva em `/private/` no checkout local. | - -Regra de ouro: o que um cloud agent não deveria ver no GitHub público também não deve ser commitado por um agente local. +`pnpm agent:file-miss` → harvest → `docs/GUARDRAILS.md`. Always-on: `AGENTS.md` → codebase-map → engineering-standards → esta página → PRODUCT/DESIGN/CUSTOMER. diff --git a/docs/CUTOVER-MAIN-ONLY.md b/docs/CUTOVER-MAIN-ONLY.md new file mode 100644 index 00000000..12820206 --- /dev/null +++ b/docs/CUTOVER-MAIN-ONLY.md @@ -0,0 +1,23 @@ +# Cutover checklist (main-only + gated deploy) — human, once +# +# After merging the CI cutover PR to main: +# +# 1. Repo secrets (Settings → Secrets and variables → Actions): +# - VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID +# - POOL_GITHUB_TOKEN (PAT: actions:write, issues:write, contents:read) +# +# 2. Branch protection: +# pnpm configure:branch-protection +# (main: required checks `checks` + `migration-lock`, strict=false, 0 reviews) +# Optionally remove protection from `stage` and delete the branch when idle. +# +# 3. Open PRs still targeting `stage`: +# gh pr list --base stage +# Retarget each to main (`gh pr edit <N> --base main`) or merge/close first. +# +# 4. Validate: +# - Push/no-op on main → Vercel Git build SKIPPED (ignoreCommand exit 0) +# - ci.yml runs full suite; deploy job runs (or fail-closed if Vercel secrets missing) +# - agent-pool doctor: `POOL_GITHUB_TOKEN=… pnpm agent:pool -- doctor` reads variables +# +# See docs/AGENT-OPS.md. diff --git a/scripts/agent-claim.mjs b/scripts/agent-claim.mjs index 6cdf32ed..5e5e0827 100644 --- a/scripts/agent-claim.mjs +++ b/scripts/agent-claim.mjs @@ -11,7 +11,7 @@ * * Output: the brief on stdout (issue, id, priority, plan/spec body) — the * agent reads it and starts; it does NOT create the branch (Cursor worktrees - * own that) and it never merges anywhere. PRs go `--base stage`. + * own that) and it never merges anywhere. PRs go `--base main`. */ import { @@ -119,6 +119,6 @@ console.log(` url: https://github.com/fsolla/teqo/issues/${pick.issue.number}`) console.log('\n--- spec ---\n') console.log(rest.trim() || '(empty body — see linked plan)') console.log( - '\n[agent:claim] Fluxo: rename_chat (cursor-app-control) → implementar → gate:fast → pnpm push -u origin HEAD → gh pr create --base stage (Closes #' + + '\n[agent:claim] Fluxo: rename_chat (cursor-app-control) → implementar → gate:fast → pnpm push -u origin HEAD → gh pr create --base main (Closes #' + `${pick.issue.number}) → PARAR. Promote stage→main é automático após CI stage green.`, ) diff --git a/scripts/agent-pool.mjs b/scripts/agent-pool.mjs index b3b66409..38eb9906 100644 --- a/scripts/agent-pool.mjs +++ b/scripts/agent-pool.mjs @@ -14,8 +14,8 @@ * pnpm agent:pool -- doctor # pré-requisitos (gh, variables, CURSOR_API_KEY, modelos) * pnpm agent:pool -- start|stop|pause|resume [-- --ref <branch>] [-- --max-slots N] * - * Promote stage→main is never part of the pool — it stays human - * (`pnpm agent:promote -- --i-am-human`). + * Promote is not part of the pool — workers PR directly to main. + * (legacy; removed from the happy path). */ import { appendFileSync } from 'node:fs' @@ -68,7 +68,7 @@ import { } from './lib/agent-pool-state.mjs' const REPO_URL = 'https://github.com/fsolla/teqo' -const POOL_BASE_REF = 'stage' +const POOL_BASE_REF = 'main' const die = dieAgent('pool') const { flags, positional } = parseArgs( @@ -348,7 +348,7 @@ const runTick = async ({ dryRun }) => { await archiveCursorAgent(spawn.agentId).catch(() => {}) } log(` merge concluído #${claim.issue.number} — slot livre, agente arquivado.`) - summary(`- #${claim.issue.number} mergeada em stage — slot livre`) + summary(`- #${claim.issue.number} mergeada em main — slot livre`) } const active = reconciliation.active @@ -478,12 +478,22 @@ const runDoctor = async () => { } catch { log(' gh auth: FALHOU (rode `gh auth login`)') } + if (process.env.POOL_GITHUB_TOKEN) { + log(' POOL_GITHUB_TOKEN: presente no env (usará este token para variables)') + } else if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) { + log( + ' POOL_GITHUB_TOKEN: ausente — usando GH_TOKEN/GITHUB_TOKEN (GHA: configure o secret POOL_GITHUB_TOKEN se variables der 403)', + ) + } else { + log(' POOL_GITHUB_TOKEN / GH_TOKEN: ausente') + } try { const vars = readPoolVariables() log(` repo variables: OK (${Object.keys(vars).length} lidas)`) - } catch { + } catch (error) { + const message = error instanceof Error ? error.message : String(error) log( - ' repo variables: SEM PERMISSÃO (precisa de actions:read; GHA usa GITHUB_TOKEN com actions:write)', + ` repo variables: FALHOU (${message}). No Actions, use secrets.POOL_GITHUB_TOKEN (PAT com actions:write + issues:write) — GITHUB_TOKEN costuma 403 em /actions/variables.`, ) } if (!process.env.CURSOR_API_KEY) { diff --git a/scripts/agent-promote.mjs b/scripts/agent-promote.mjs index af7bc44f..ef6eb2fc 100644 --- a/scripts/agent-promote.mjs +++ b/scripts/agent-promote.mjs @@ -1,175 +1,20 @@ /** - * `pnpm agent:promote -- --i-am-human` — manual override: promote `stage` → `main`. + * `pnpm agent:promote` — REMOVED (main-only cutover). * - * Normal path: `promote-stage-to-main.yml` auto-merges when ci-stage is green on - * the current stage head. Use this when the workflow failed or you need to promote - * out of band — including when stage head is red but an older commit on stage - * already passed ci-stage. + * Feature PRs merge directly to `main`. Production deploy is gated by `ci.yml` + * (`vercel deploy --prod` after the full suite). There is no stage→main promote. * - * Checks (in order): - * 1. `main` has not diverged from `stage` (hotfix direto em main exige merge - * de main em stage antes). - * 2. Newest commit on `origin/main..origin/stage` with a successful ci-stage run. - * 3. PR into `main` from `stage` (head green) or `promote/last-green` (older green). - * 4. Merge (ci-stage on the promoted SHA is the pre-promote gate; ci.yml on main - * is the post-merge safety net). + * See docs/AGENT-OPS.md. */ -import { execFileSync } from 'node:child_process' - -import { dieAgent, gh, ghJson, parseArgs } from './lib/agent-github.mjs' -import { - findLastGreenPromoteSha, - greenCiStageHeadShas, - PROMOTE_GREEN_BRANCH, -} from './lib/agent-promote-target.mjs' +import { dieAgent, parseArgs } from './lib/agent-github.mjs' const die = dieAgent('promote') -const { flags } = parseArgs(process.argv.slice(2), new Set()) +parseArgs(process.argv.slice(2), new Set()) -if (flags['i-am-human'] !== true) { - die( - 'Refused: promote override is human-only. Re-run as a human with:\n' + - ' pnpm agent:promote -- --i-am-human\n' + - 'Auto-promote runs via promote-stage-to-main.yml when ci-stage is green on stage head.', - ) -} - -const git = (args) => execFileSync('git', args, { encoding: 'utf8' }).trim() -git(['fetch', 'origin', 'main', 'stage']) - -// 1. Divergence: main must be an ancestor of stage. -try { - git(['merge-base', '--is-ancestor', 'origin/main', 'origin/stage']) -} catch { - die( - '`main` divergiu de `stage` (hotfix direto em main?). Antes do promote:\n' + - ' git checkout stage && git merge origin/main && git push origin stage\n' + - '…e espere o CI stage ficar verde de novo.', - ) -} - -const stageSha = git(['rev-parse', 'origin/stage']) -const mainSha = git(['rev-parse', 'origin/main']) - -if (stageSha === mainSha) { - die(`main já está no head de stage (${stageSha.slice(0, 8)}) — nada a promover.`) -} - -const commitsAhead = git(['log', `origin/main..origin/stage`, '--format=%H']) - .split('\n') - .filter(Boolean) - -const stageRuns = ghJson([ - 'run', - 'list', - '--branch', - 'stage', - '--workflow', - 'ci-stage.yml', - '--limit', - '100', - '--json', - 'headSha,status,conclusion,url', -]) - -const promoteSha = findLastGreenPromoteSha(commitsAhead, greenCiStageHeadShas(stageRuns)) - -if (!promoteSha) { - die( - `Nenhum commit em stage à frente de main tem ci-stage verde (stage head ${stageSha.slice(0, 8)}). ` + - 'Corrija stage ou espere um run verde antes do promote.', - ) -} - -const promoteRun = stageRuns.find( - (run) => run.headSha === promoteSha && run.status === 'completed' && run.conclusion === 'success', -) - -if (promoteSha === stageSha) { - console.log(`[agent:promote] CI stage green no head de stage (${stageSha.slice(0, 8)}) ✓`) -} else { - console.log( - `[agent:promote] stage head ${stageSha.slice(0, 8)} não está verde; ` + - `promovendo último commit ci-stage green ${promoteSha.slice(0, 8)} ✓`, - ) - if (promoteRun?.url) { - console.log(`[agent:promote] run: ${promoteRun.url}`) - } -} - -const prHead = promoteSha === stageSha ? 'stage' : PROMOTE_GREEN_BRANCH - -if (prHead !== 'stage') { - execFileSync( - 'git', - ['push', 'origin', `${promoteSha}:refs/heads/${PROMOTE_GREEN_BRANCH}`, '--force'], - { - stdio: 'inherit', - }, - ) -} - -// 3. PR → main (create if needed). -let pr = ghJson([ - 'pr', - 'list', - '--state', - 'open', - '--base', - 'main', - '--head', - prHead, - '--json', - 'number,url', -])[0] - -if (!pr && prHead !== 'stage') { - const staleStagePr = ghJson([ - 'pr', - 'list', - '--state', - 'open', - '--base', - 'main', - '--head', - 'stage', - '--json', - 'number,url', - ])[0] - if (staleStagePr) { - console.log( - `[agent:promote] fechando PR #${staleStagePr.number} stage→main (head vermelho; promote em ${promoteSha.slice(0, 8)})`, - ) - gh(['pr', 'close', String(staleStagePr.number)]) - } -} - -if (!pr) { - const body = - promoteSha === stageSha - ? 'Override manual via `pnpm agent:promote --i-am-human`. CI stage green no head de stage.' - : `Override manual via \`pnpm agent:promote --i-am-human\`. Promove ci-stage green \`${promoteSha.slice(0, 8)}\` (stage head \`${stageSha.slice(0, 8)}\` não está verde).` - const url = gh([ - 'pr', - 'create', - '--base', - 'main', - '--head', - prHead, - '--title', - `Promote stage → main (${new Date().toISOString().slice(0, 10)})`, - '--body', - body, - ]) - pr = { number: url.split('/').pop(), url } - console.log(`[agent:promote] created PR ${pr.url}`) -} else { - console.log(`[agent:promote] reusing open PR ${pr.url}`) -} - -// 4. Merge (ci-stage on promoteSha was the pre-promote gate; ci.yml on main is the safety net). -gh(['pr', 'merge', String(pr.number), '--admin', '--merge']) -console.log( - `[agent:promote] merged ${prHead} (${promoteSha.slice(0, 8)}) → main via PR #${pr.number}. Deploy Vercel a caminho.`, +die( + 'Removed: stage→main promote no longer exists.\n' + + ' Open PRs with `gh pr create --base main`.\n' + + ' Production deploy runs from `.github/workflows/ci.yml` after the full verifier is green.\n' + + ' Details: docs/AGENT-OPS.md', ) diff --git a/scripts/configure-branch-protection.mjs b/scripts/configure-branch-protection.mjs index b6c74215..b33d3128 100644 --- a/scripts/configure-branch-protection.mjs +++ b/scripts/configure-branch-protection.mjs @@ -1,15 +1,11 @@ /** - * One-time / drift repair for branch protection aligned with auto-promote + parallel agents. + * One-time / drift repair for branch protection (main-only). * * pnpm configure:branch-protection # apply * pnpm configure:branch-protection -- --dry-run * - * Changes: - * - stage: strict=false (allow merge without being up-to-date; conflicts still block) - * - main: required_approving_review_count=0 (auto-promote replaces human review) - * - * Auto-promote still needs secrets.PROMOTE_GITHUB_TOKEN (admin PAT) on the repo if - * GITHUB_TOKEN cannot bypass main protection — see docs/AGENT-OPS.md. + * main: required checks `checks` + `migration-lock`, strict=false, 0 reviews. + * Feature PRs auto-merge when those contexts are green. */ import { execFileSync } from 'node:child_process' @@ -41,13 +37,20 @@ const ghApiJson = (method, path, body) => { console.log(`[configure:branch-protection] repo ${repo}${dryRun ? ' (dry-run)' : ''}`) -ghApiJson('PATCH', `repos/${repo}/branches/stage/protection/required_status_checks`, { - strict: false, - contexts: ['checks', 'migration-lock'], +ghApiJson('PUT', `repos/${repo}/branches/main/protection`, { + required_status_checks: { + strict: false, + contexts: ['checks', 'migration-lock'], + }, + enforce_admins: false, + required_pull_request_reviews: { + required_approving_review_count: 0, + }, + restrictions: null, + allow_force_pushes: false, + allow_deletions: false, }) -ghApiJson('PATCH', `repos/${repo}/branches/main/protection/required_pull_request_reviews`, { - required_approving_review_count: 0, -}) - -console.log('[configure:branch-protection] done.') +console.log( + '[configure:branch-protection] done (main: checks + migration-lock, strict=false, 0 reviews).', +) diff --git a/scripts/lib/agent-pool-prompt.mjs b/scripts/lib/agent-pool-prompt.mjs index b1e5acdd..32d9a1d4 100644 --- a/scripts/lib/agent-pool-prompt.mjs +++ b/scripts/lib/agent-pool-prompt.mjs @@ -5,8 +5,8 @@ * The supervisor claims the issue BEFORE spawning (label flip + pool-worker * marker comment — the coordinated-claim anti-race), so the worker SKIPS the * claim step of work-issue and starts at step 1b. Everything else — gates, PR - * base stage with Closes #N, auto-merge + CI watch, never promote — is the - * standard skill contract restated so the worker cannot miss it. + * base main with Closes #N, auto-merge + CI watch — is the standard skill + * contract restated so the worker cannot miss it. */ /** First docs/plans/*.md path in the issue body (the linked plan), if any. */ @@ -37,13 +37,12 @@ export const buildPoolWorkerPrompt = ({ issueNumber, issueTitle, issueId, planPa ? `2. Plano: \`${planPath}\` — freshness audit enxuto (passo 3 da skill).` : '2. O body da Issue é a spec (não há plano linkado — avalie se ele basta para trabalhar).', '3. Execute as fases com os gates do repo: `pnpm gate:fast` antes do push (o pre-push roda `pnpm gate:push`). Comandos bare, nunca piped.', - `4. PR obrigatoriamente com \`gh pr create --base stage\` e "Closes #${issueNumber}" no body.`, - '5. `gh pr merge --auto --merge <PR>` e acompanhe `gh pr checks <PR> --watch` até o merge. Falha de CI no seu PR é sua (docs/AGENT-OPS.md — "Dono do PR, dono do CI"): corrija na mesma branch.', + `4. PR obrigatoriamente com \`gh pr create --base main\` e "Closes #${issueNumber}" no body.`, + '5. `gh pr merge --auto --merge <PR>` e acompanhe `gh pr checks <PR> --watch --required` até o merge. Falha de CI no seu PR é sua (docs/AGENT-OPS.md — "Dono do PR, dono do CI"): corrija na mesma branch.', '', '## Proibido', '', - '- `pnpm agent:promote` — promote stage→main é humano.', - '- `DATABASE_URL` de stage/prod ou `ALLOW_REMOTE_DB` — o setup local (`.cursor/cloud-setup.sh` + seed mínimo) cobre tudo.', + '- `DATABASE_URL` de prod ou `ALLOW_REMOTE_DB` — o setup local (`.cursor/cloud-setup.sh` + seed mínimo) cobre tudo.', `- Editar outras Issues \`in-progress\` ou trabalhar fora da Issue #${issueNumber}.`, '', modelSlug diff --git a/scripts/lib/agent-promote-target.mjs b/scripts/lib/agent-promote-target.mjs deleted file mode 100644 index 0dc7bec8..00000000 --- a/scripts/lib/agent-promote-target.mjs +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Pure helpers for `agent-promote`: pick the newest commit on stage (ahead of main) - * that has a successful ci-stage run — not necessarily the current stage head. - */ - -/** Branch used when promoting a green commit behind a red stage head. */ -export const PROMOTE_GREEN_BRANCH = 'promote/last-green' - -/** - * @param {string[]} commitsAhead newest-first SHAs on origin/main..origin/stage - * @param {ReadonlySet<string>} greenShas head SHAs with ci-stage success - * @returns {string | null} - */ -export function findLastGreenPromoteSha(commitsAhead, greenShas) { - for (const sha of commitsAhead) { - if (greenShas.has(sha)) return sha - } - return null -} - -/** - * @param {Array<{ headSha: string; status: string; conclusion: string | null }>} stageRuns - * @returns {Set<string>} - */ -export function greenCiStageHeadShas(stageRuns) { - const green = new Set() - for (const run of stageRuns) { - if (run.status === 'completed' && run.conclusion === 'success') { - green.add(run.headSha) - } - } - return green -} diff --git a/scripts/vercel-ignore-build.sh b/scripts/vercel-ignore-build.sh index f304f51a..210a4f04 100755 --- a/scripts/vercel-ignore-build.sh +++ b/scripts/vercel-ignore-build.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash # Vercel "Ignored Build Step": exit 0 = skip deployment, exit 1 = build. -# Agent feature branches must not queue preview builds (Hobby = 1 concurrent build). -# stage/main always build (stage preview + prod). +# Production deploys are gated by GitHub Actions (`ci.yml` → `vercel deploy --prod`). +# Git pushes must NOT trigger Vercel builds (Hobby concurrency + ungated deploy race). set -euo pipefail ref="${VERCEL_GIT_COMMIT_REF:-}" if [ "$ref" = "main" ] || [ "$ref" = "stage" ]; then - exit 1 + echo "skip: $ref — production deploy is Actions-gated (ci.yml), not Vercel Git" + exit 0 fi if [[ "$ref" == agent/* ]] || [[ "$ref" == cursor/* ]]; then @@ -15,4 +16,5 @@ if [[ "$ref" == agent/* ]] || [[ "$ref" == cursor/* ]]; then exit 0 fi -exit 1 +echo "skip: non-canonical branch ($ref) — no Vercel Git builds" +exit 0 diff --git a/tests/helpers/assertTestDatabase.ts b/tests/helpers/assertTestDatabase.ts index 79eaeccd..1717bc96 100644 --- a/tests/helpers/assertTestDatabase.ts +++ b/tests/helpers/assertTestDatabase.ts @@ -24,18 +24,6 @@ export function assertTestDatabase(databaseUrl: string | undefined): void { const databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, '')) const localHosts = new Set(['localhost', '127.0.0.1', '::1', '[::1]']) - // Documented stage escape hatch: ONLY the ci-stage workflow (GitHub - // Environment `stage`, STAGE_DATABASE_URL secret) may point the int suite at - // the stage Neon snapshot, and it sets this flag explicitly. Agents and local - // runs never set it — the default stays the exact local teqo_test. - if (process.env.ALLOW_STAGE_TEST_DB === 'true') { - if (parsedUrl.protocol === 'postgresql:' && databaseName.length > 0) return - throw new Error( - 'ALLOW_STAGE_TEST_DB is set but DATABASE_URL is not a postgresql: connection string ' + - `with a database name (got "${parsedUrl.protocol || '(unknown)'}").`, - ) - } - if ( parsedUrl.protocol !== 'postgresql:' || databaseName !== 'teqo_test' || diff --git a/tests/unit/agentPoolPrompt.unit.spec.ts b/tests/unit/agentPoolPrompt.unit.spec.ts index a086184b..7fec64ef 100644 --- a/tests/unit/agentPoolPrompt.unit.spec.ts +++ b/tests/unit/agentPoolPrompt.unit.spec.ts @@ -19,15 +19,14 @@ describe('buildPoolWorkerPrompt', () => { expect(prompt).toContain('in-progress') }) - it('pins the PR contract: base stage, Closes #N, auto-merge, CI watch', () => { - expect(prompt).toContain('gh pr create --base stage') + it('pins the PR contract: base main, Closes #N, auto-merge, CI watch', () => { + expect(prompt).toContain('gh pr create --base main') expect(prompt).toContain('Closes #42') expect(prompt).toContain('gh pr merge --auto --merge') expect(prompt).toContain('gh pr checks <PR> --watch') }) - it('restates the hard prohibitions (promote humano, sem DB remota, escopo da Issue)', () => { - expect(prompt).toContain('pnpm agent:promote') + it('restates the hard prohibitions (sem DB remota, escopo da Issue)', () => { expect(prompt).toContain('DATABASE_URL') expect(prompt).toContain('ALLOW_REMOTE_DB') expect(prompt).toContain('trabalhar fora da Issue #42') diff --git a/tests/unit/agentPromoteTarget.unit.spec.ts b/tests/unit/agentPromoteTarget.unit.spec.ts deleted file mode 100644 index d2703140..00000000 --- a/tests/unit/agentPromoteTarget.unit.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { - findLastGreenPromoteSha, - greenCiStageHeadShas, -} from '../../scripts/lib/agent-promote-target.mjs' - -describe('greenCiStageHeadShas', () => { - it('collects only completed successes', () => { - const green = greenCiStageHeadShas([ - { headSha: 'aaa', status: 'completed', conclusion: 'success' }, - { headSha: 'bbb', status: 'completed', conclusion: 'failure' }, - { headSha: 'ccc', status: 'in_progress', conclusion: null }, - ]) - expect([...green]).toEqual(['aaa']) - }) -}) - -describe('findLastGreenPromoteSha', () => { - it('returns the newest green commit ahead of main', () => { - const green = new Set(['bbb', 'ddd']) - expect(findLastGreenPromoteSha(['ccc', 'bbb', 'aaa'], green)).toBe('bbb') - }) - - it('returns stage head when it is green', () => { - const green = new Set(['ccc']) - expect(findLastGreenPromoteSha(['ccc', 'bbb'], green)).toBe('ccc') - }) - - it('returns null when nothing ahead is green', () => { - const green = new Set(['zzz']) - expect(findLastGreenPromoteSha(['ccc', 'bbb'], green)).toBeNull() - }) - - it('returns null when there is nothing ahead of main', () => { - expect(findLastGreenPromoteSha([], new Set(['aaa']))).toBeNull() - }) -})