Przejdź do głównej zawartości

Bramy jakości dla kodu pisanego przez agentów

Brama jakości dla kodu pisanego przez agentów to jeden zestaw deterministycznych kontroli (formatowanie, lint, sprawdzanie typów, testy i analiza statyczna nowego kodu), uruchamiany trzy razy: po każdej edycji agenta, zanim agent zgłosi „gotowe”, i jako wymagany job CI blokujący merge. Code review przez AI działa obok bramy jako rada, nigdy jako sama brama.

Agenci w twoim zespole otwierają piętnaście pull requestów dziennie. Lint działa tylko w CI, dwadzieścia minut po pushu, więc agent czyta błąd dopiero rundę później i „naprawia” go przez // eslint-disable-next-line. Recenzent, który nie jest w stanie przeczytać piętnastu diffów, zatwierdza te zielone. Po sześciu tygodniach w kodzie jest 140 nowych wyciszeń, a nikt nie zdecydował o żadnym z nich.

Ta strona jest dla developerów, którzy podpinają kontrole, i tech leadów, którzy za nie odpowiadają. Obejmuje mechaniczną połowę weryfikacji: czy kod jest poprawnie zbudowany. To, czy kod robi to, co powinien, sprawdzają testy i miary z siły wyroczni.

  • Jedno polecenie check, które jest bramą i które agent, hooki i CI uruchamiają identycznie
  • Konfigurację ESLint, w której każde wyciszenie musi mieć pisemne uzasadnienie, przetestowaną na ESLint 10
  • Rejestrację hooków w Claude Code i Codeksie, która zwraca błędy agentowi, zanim zobaczy je człowiek, oraz odpowiednik dla Cursora
  • Workflow GitHub Actions z wymaganym jobem check i bramą SonarQube na nowym kodzie, bez sekretów w pobliżu kodu z pull requesta
  • Cztery prompty do skopiowania: przejdź bramę bez jej osłabiania, zamień uwagę z review w regułę, zrób audyt bramy i zrób triaż wyników analizy statycznej
  • Pięć miar, które mówią, czy brama działa, i kto zatwierdza zmiany w niej

Raport DORA z 2025 roku stwierdza, że wdrożenie AI ma „pozytywny związek” z przepustowością dostarczania, ale „nadal ma negatywny związek ze stabilnością dostarczania oprogramowania”. Wskazuje też przyczynę: bez solidnych mechanizmów kontroli, takich jak dobre testy automatyczne, dojrzałe praktyki kontroli wersji i szybkie pętle informacji zwrotnej, wzrost liczby zmian prowadzi do niestabilności (blog Google Cloud DORA, 2025-09-23).

Tę samą lukę widać w deklaracjach developerów. W ankiecie Sonar wśród ponad 1100 zawodowych developerów 96% nie ufa w pełni kodowi wygenerowanemu przez AI, a tylko 48% zawsze weryfikuje go przed commitem (Sonar, 2026-01-08). Brama jakości zamyka tę lukę mechanicznie. Kontrola działa niezależnie od tego, czy ktoś pamięta, żeby ją uruchomić, a agent nie zakończy tury ani nie zmerge’uje zmiany, dopóki kontrola nie jest zielona.

Blokuj tylko na kontrolach deterministycznych i uruchamiaj każdą w najwcześniejszym miejscu, które jeszcze może zatrzymać defekt. Warstwy doradcze mogą zadawać pytania, ale nigdy nie decydują o merge’u.

WarstwaKiedy działaCzy blokuje?Co łapieWłaściciel
Hook po edycjiPo każdej edycji plikuZwraca błędy agentowiBłędy formatowania i lintu we właśnie edytowanym plikuDeveloper
Brama przy zatrzymaniuGdy agent próbuje zakończyć turęRaz odsyła agenta do pracyNiezaliczone check w dowolnym miejscuDeveloper
Wymagany job CI checkKażdy pull requestTak, przez ochronę gałęziWszystko powyżej, na czystym runnerze, niezależnie od tego, jakie narzędzie napisało kodTech lead
Analiza statyczna nowego koduKażdy pull requestTak, jako drugi wymagany checkBłędy, podatności, duplikację i pokrycie zmienionych liniiTech lead
Code review przez AIKażdy pull requestNie, doradczoProblemy z logiką i intencją, których nie koduje jeszcze żadna regułaRecenzent

Sąsiednie bramy podpina się do tej samej listy wymaganych checków: funkcje dopasowania architektury dla warstw, budżetów złożoności i duplikacji; wykrywanie slopu dla wzorców wiarygodnych, ale błędnych; kontrolę zależności dla zmyślonych pakietów. Ta strona buduje podstawę, do której podłącza się pozostałe.

  1. Schowaj całą bramę za jednym poleceniem. Jeśli hook uruchamia jedno polecenie, a CI inne, agent nauczy się przechodzić hook, a CI dalej będzie czerwone. Dodaj to do package.json (przykład dla TypeScriptu; odpowiedniki dla Pythona i Go są w tabeli pod krokami):

    {
    "scripts": {
    "format:check": "prettier --check .",
    "lint": "eslint --max-warnings=0 .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "check": "npm run format:check && npm run lint && npm run typecheck && npm test"
    }
    }

    --max-warnings=0 zamienia każde ostrzeżenie w błąd. Ostrzeżenie, które brama toleruje, to ostrzeżenie, które agent nauczy się ignorować.

  2. Wymagaj uzasadnienia przy każdym wyciszeniu. Agent przed czerwoną bramą sięga po eslint-disable i @ts-ignore. Wyciszenia bywają uzasadnione, więc ich nie zakazuj; każ każdemu wyjaśniać samo siebie, żeby recenzent mógł przeszukać uzasadnienia. Tę konfigurację uruchomiliśmy 2026-09-26 na ESLint 10.11.0, typescript-eslint 8.71.0 i @eslint-community/eslint-plugin-eslint-comments 4.8.1:

    // eslint.config.js: the lint half of the gate (ESLint 10, flat config)
    import { defineConfig } from 'eslint/config';
    import js from '@eslint/js';
    import tseslint from 'typescript-eslint';
    import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
    export default defineConfig(
    js.configs.recommended,
    tseslint.configs.recommended,
    comments.recommended,
    {
    linterOptions: { reportUnusedDisableDirectives: 'error' },
    rules: {
    // A suppression must say why, so a reviewer can grep the reasons.
    '@eslint-community/eslint-comments/require-description': 'error',
    // @ts-ignore is banned; @ts-expect-error needs a 10+ character reason.
    '@typescript-eslint/ban-ts-comment': [
    'error',
    { 'ts-expect-error': 'allow-with-description', 'ts-ignore': true, minimumDescriptionLength: 10 },
    ],
    '@typescript-eslint/no-explicit-any': 'error',
    },
    },
    );

    Zainstaluj pakiety przez npm i -D eslint @eslint/js typescript-eslint typescript @eslint-community/eslint-plugin-eslint-comments i ustaw "type": "module" w package.json (albo nazwij plik eslint.config.mjs). W naszym przebiegu gołe // eslint-disable-next-line i gołe // @ts-ignore nie przeszły. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- third-party typings are wrong, PROJ-9 przeszło.

  3. Powiedz agentowi, że brama istnieje. Dodaj trzy linie do CLAUDE.md, AGENTS.md albo reguły projektu w Cursorze, zależnie od tego, co czytają twoje narzędzia:

    ## Definition of done
    - `npm run check` passes. Run it before you say a task is finished.
    - Never add `eslint-disable`, `@ts-expect-error`, `# noqa` or `# type: ignore` without a reason after `--`, and list each one you add in your summary.
    - Never edit eslint.config.js, tsconfig.json, .prettierrc, sonar-project.properties or .github/ to make a check pass. Stop and explain instead.

    Instrukcje to rada, której agent zwykle słucha. Egzekwują ją kroki 4 i 5.

  4. Uruchamiaj kontrole w pętli agenta. Zarejestruj hook po edycji i bramę przy zatrzymaniu w każdym narzędziu, jak w następnej sekcji. Agent widzi wtedy błąd kilka sekund po tym, jak go spowodował, gdy kontekst, który do niego doprowadził, jest jeszcze w sesji.

  5. Zrób z CI bramę, której nie da się pominąć. Dodaj workflow poniżej i oznacz check oraz sonar jako wymagane status checki w regule ochrony gałęzi albo rulesecie domyślnej gałęzi. Hooki działają tylko tam, gdzie działa agent. Wymagany check działa dla każdego pull requesta, także od agenta w chmurze czy od kolegi bez zainstalowanych hooków.

  6. Chroń pliki samej bramy. Dodaj eslint.config.js, tsconfig.json, .prettierrc, sonar-project.properties i .github/ do CODEOWNERS z zespołem platformowym albo leadami jako właścicielem i włącz Require review from Code Owners. Jak zablokować te pliki w każdym narzędziu i jak uruchamiać kontrole z workflow, którego pull request nie może edytować, opisuje ochrona wyroczni.

  7. Udowodnij, że brama robi się czerwona. Otwórz próbny pull request z jednym celowym naruszeniem na każdą kontrolę: źle sformatowany plik, nieużywana zmienna, błąd typu, gołe @ts-ignore, niezaliczony test. Każda kontrola musi się wysypać, a przycisk merge’a musi zostać nieaktywny. Powtórz to po każdej zmianie bramy.

Ten sam wzorzec w innych stosach:

StosFormatowanieLintTypyReguła dla wyciszeń
TypeScriptprettier --check .eslint --max-warnings=0 .tsc --noEmitrequire-description, ban-ts-comment (powyżej)
Pythonruff format --check .ruff check .mypy .Reguły Ruff PGH003 i PGH004 odrzucają ogólne # type: ignore i # noqa i wymuszają konkretny kod
Gogofmt -l . (błąd, gdy wypisze cokolwiek)golangci-lint runkompilator i go vet ./...nolintlint w golangci-lint z require-explanation: true

Oba CLI stosują ten sam kontrakt: hook, który kończy się kodem 2, odsyła swój stderr agentowi. Po edycji (PostToolUse) plik jest już zapisany, więc agent poprawia go w następnym kroku. Przy Stop agent pracuje dalej, zamiast kończyć turę. Kod 1, awaria albo timeout przepuszczają akcję, więc hook to warstwa szybkiej informacji zwrotnej, a nie punkt egzekwowania. Pełny zestaw czterech hooków z fixture’ami, które dowodzą, że każdy z nich blokuje, opisuje strona hooki jako deterministyczne zabezpieczenia.

Brama przy zatrzymaniu jest wspólna dla obu CLI. Uruchamia całe check raz, a jeśli agent nie potrafi naprawić błędu, oddaje sprawę człowiekowi, więc nigdy się nie zapętla:

#!/usr/bin/env bash
# .agent-hooks/stop-check.sh: Stop hook for Claude Code and Codex
set -uo pipefail
again=$(jq -r '.stop_hook_active // false' 2>/dev/null || echo false)
[ "$again" = "true" ] && exit 0 # second attempt: let the turn end, CI decides
cd "$(git rev-parse --show-toplevel)" || exit 0
if ! out=$(npm run --silent check 2>&1); then
printf 'npm run check fails. Fix the cause; do not suppress it or edit gate config.\n%s\n' \
"$(printf '%s' "$out" | tail -n 40)" >&2
exit 2
fi
exit 0

Nadaj mu prawo wykonania przez chmod +x .agent-hooks/stop-check.sh. Wymaga jq w PATH.

Zarejestruj hook lintujący po edycji i bramę przy zatrzymaniu w .claude/settings.json (sprawdzone na Claude Code 2.1.283). Matcher to wyrażenie regularne na nazwie narzędzia; timeouty są w sekundach.

{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/lint-edited-file.sh", "timeout": 60 }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.agent-hooks/stop-check.sh", "timeout": 600 }] }
]
}
}

Hook po edycji czyta ścieżkę edytowanego pliku z tool_input.file_path na stdin i sprawdza tylko ten plik. tsc dla całego projektu należy do bramy przy zatrzymaniu, nie tutaj: przy każdej edycji raportowałby od nowa każdy istniejący błąd w repozytorium.

#!/usr/bin/env bash
# .claude/hooks/lint-edited-file.sh: PostToolUse, Edit|Write
set -uo pipefail
file=$(jq -r '.tool_input.file_path // empty' 2>/dev/null) || exit 0
case "$file" in *.ts|*.tsx|*.js|*.jsx) ;; *) exit 0 ;; esac
bin="$CLAUDE_PROJECT_DIR/node_modules/.bin"
"$bin/prettier" --write "$file" >/dev/null 2>&1 || true # formatting repairs silently
if ! out=$("$bin/eslint" --max-warnings=0 "$file" 2>&1); then
printf 'ESLint fails on %s. Fix it without adding a suppression:\n%s\n' "$file" "$out" | head -c 4000 >&2
exit 2
fi
exit 0

Wpisz /hooks w sesji, żeby potwierdzić, że oba hooki się załadowały i z którego pliku ustawień pochodzą.

Workflow ma dwa joby. check uruchamia kod z pull requesta bez sekretów i z tokenem tylko do odczytu. sonar trzyma token Sonara i nie uruchamia kodu projektu: czyta pliki i raport pokrycia wygenerowany przez check.

.github/workflows/quality.yml
name: quality
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run check
- name: Coverage for Sonar
run: npx vitest run --coverage --coverage.reporter=lcov
- name: Flag changes to the gate's own config
if: github.event_name == 'pull_request'
run: |
changed=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- \
eslint.config.js tsconfig.json .prettierrc sonar-project.properties .github/)
[ -z "$changed" ] || echo "::warning::This pull request changes gate config: $changed"
- uses: actions/upload-artifact@v7
with:
name: coverage
path: coverage/lcov.info
sonar:
needs: check
# Fork pull requests get no secrets, so skip rather than fail.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: coverage
path: coverage
- uses: SonarSource/sonarqube-scan-action@v8
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- uses: SonarSource/sonarqube-quality-gate-action@v1
with:
pollingTimeoutSec: 600
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

CI wywołuje to samo npm run check co hooki, więc kontrola dodana do tego skryptu trafia do CI bez zmiany workflow. Krok pokrycia uruchamia testy drugi raz tylko po to, żeby zapisać raport, który czyta Sonar; nie blokuje niczego, czego nie zablokowało już check. Krok pokrycia wymaga @vitest/coverage-v8 w devDependencies. Wskaż Sonarowi raport pokrycia w sonar-project.properties przez sonar.javascript.lcov.reportPaths=coverage/lcov.info. Akcja quality gate kończy job błędem, gdy quality gate SonarQube Server nie przechodzi, więc sonar staje się blokującym checkiem. W SonarQube Cloud usuń SONAR_HOST_URL i skonfiguruj sprawdzanie quality gate według instrukcji Sonara. Główne wersje akcji sprawdziliśmy 2026-09-26: checkout@v7, setup-node@v7, upload-artifact@v7, download-artifact@v8, sonarqube-scan-action@v8 i sonarqube-quality-gate-action@v1.

Ustaw quality gate na nowym kodzie, nie na całym kodzie. Brama na całym kodzie wysypuje każdy pull request w repozytorium legacy, dopóki ktoś nie naprawi lat zaległości, a zespół uczy się ją ignorować. Brama na nowym kodzie trzyma każdą zmianę w standardzie od dziś. Podejście z zapadką dla lintu i złożoności opisuje sekcja dlaczego zapadka wygrywa z progiem w istniejącym kodzie.

Dwie uwagi o bezpieczeństwie, bo czytelnicy kopiują ten workflow bez zmian. npm ci uruchamia skrypty cyklu życia z pull requesta, dlatego check nie ma sekretów, a token jest tylko do odczytu. Job sonar czyta sonar-project.properties z pull requesta, dlatego ten plik jest w CODEOWNERS z kroku 6.

Code review przez AI znajduje to, czego nie koduje jeszcze żadna reguła: błędne założenie, pominięty przypadek brzegowy, zmianę niezgodną ze specyfikacją. Generuje też fałszywe alarmy i pomija błędy w stopniu, którego nie przewidzisz dla konkretnego pull requesta. Dlatego nigdy nie blokuje. Komentuje, a człowiek, który zatwierdza, czyta jego uwagi obok dowodów. Pełny triaż, łącznie z tym, które pull requesty człowiek nadal czyta, opisuje strona code review PR-a agenta bez czytania każdej linii.

W świeżej sesji na gałęzi, nie w tej, która napisała kod, uruchom /code-review dla bieżącego diffa albo /code-review high 1234 dla pull requesta 1234. --comment publikuje uwagi w pull requeście, a --fix je wprowadza. Zarządzana usługa Code Review (research preview, Team i Enterprise) czyta reguły review z pliku REVIEW.md w katalogu głównym repozytorium. Jej check run zawsze kończy się wynikiem neutralnym, więc nigdy nie blokuje merge’a przez ochronę gałęzi (sprawdzone 2026-09-26).

Pętla, dzięki której brama z czasem się wzmacnia: gdy ta sama uwaga z review przez AI pojawi się w trzech pull requestach, zamień ją w deterministyczną regułę (regułę lintu, typ, test) i przenieś z rady do bramy. Jak boty do review od różnych dostawców wypadają pod względem szumu i konfiguracji, porównuje strona boty do AI code review.

Brama, która zawsze jest zielona, sama niczego nie dowodzi. Śledź pięć miar dla każdego repozytorium co miesiąc i patrz na trend:

MiaraDefinicjaCo mówi zły trend
Dodane wyciszeniaNowe komentarze wyciszające na zmerge’owany pull request, liczone z diffaRośnie: agent wycisza bramę zamiast poprawiać kod. Zaostrz reguły z kroku 2 i definicję ukończenia
Ucieczki przez bramęDefekty na produkcji, które istniejąca reguła lintu, typów lub analizy statycznej mogła złapać, podzielone przez wszystkie defekty na produkcjiPowyżej zera: jakaś kontrola nie działa tam, gdzie myślisz. Powtórz próbny pull request z kroku 7
Awansowane regułyUwagi z review przez AI zamienione w tym miesiącu w deterministyczne regułyZero przez kilka miesięcy: brama przestała się uczyć, a ciężar niesie review
Czas do zielonego w pętliMediana tur agenta między błędem hooka a zaliczonym checkRośnie: komunikaty błędów są za długie albo za mało jasne, by agent mógł na nie zareagować
ObejściaMerge’e z czerwonym wymaganym checkiem przez obejście administratoraKażde: ktoś uznał, że brama się myli. Popraw regułę albo zapisz powód

Zatwierdzanie jest jawne. Tech lead odpowiada za konfigurację bramy przez CODEOWNERS i zmienia ją tylko w przeglądanych pull requestach. Zatwierdzający recenzent odpowiada za przeczytanie uwag z review przez AI, a nie za ponowne uruchamianie kontroli. Nikt nie merge’uje z czerwonym wymaganym checkiem inaczej niż przez zarejestrowane obejście administratora. Te miary są opisane razem z innymi liczbami zespołu w ramach metryk dla inżynierii agentowej.

Co się psuje, gdy brama pilnuje kodu pisanego przez agentów?

Dział zatytułowany „Co się psuje, gdy brama pilnuje kodu pisanego przez agentów?”

Agent wycisza bramę zamiast poprawiać kod. Dodaje // eslint-disable-next-line, luzuje tsconfig.json albo oznacza test jako .skip. Wyjście: reguły wyciszeń z kroku 2 sprawiają, że gołe wyciszenia nie przechodzą; CODEOWNERS na plikach konfiguracji ujawnia ich luzowanie; krok „Flag changes to the gate’s own config” oznacza pull request adnotacją. Osłabianie testów ma własny detektor w wykrywaniu slopu.

Pętla zwalnia do żółwiego tempa. Hook, który po każdej edycji uruchamia całe testy albo tsc dla całego projektu, dodaje minuty do każdej tury i ludzie go wyłączają. Wyjście: hooki po edycji działają na jednym pliku i mieszczą się w kilku sekundach; całe check uruchamiaj raz, przy Stop, i ponownie w CI.

Repozytorium legacy jest czerwone od pierwszego dnia. Włączenie --max-warnings=0 albo surowej quality gate ujawnia tysiące uwag i zespół wyłącza bramę. Wyjście: niech brama obejmuje tylko nowy kod (definicja nowego kodu w Sonarze, lint zmienionych plików) i zmniejszaj istniejącą liczbę zapadką, jak w funkcjach dopasowania architektury.

Wyniki lokalne i w CI się różnią. Hook przechodzi na laptopie, a CI się wysypuje, albo odwrotnie, bo wersje narzędzi są różne. Wyjście: uruchamiaj każde narzędzie z node_modules/.bin albo w wersjach przypiętych w lockfile’u, nigdy z instalacji globalnej, i używaj tego samego npm run check w obu miejscach.

Pull request edytuje workflow, który go sprawdza. Workflow pull_request uruchamia YAML z samego pull requesta, więc agent, który usunie krok lintu, i tak dostaje zielony check. Wyjście: CODEOWNERS na .github/, a dla rozstrzygających kontroli wymagany workflow trzymany w innym repozytorium, jak opisuje ochrona wyroczni.

Brama analizy statycznej nigdy nie wysypuje pull requestów. Sonar analizuje tylko main albo brama jest ustawiona na całym kodzie i już wcześniej nie przechodziła, więc nikt jej nie czyta. Wyjście: upewnij się, że działa analiza pull requestów, ustaw bramę na nowym kodzie i dodaj celowe naruszenie reguły Sonara do próbnego pull requesta z kroku 7.

Review przez AI z przyzwyczajenia staje się blokadą merge’a. Recenzenci czekają na komentarze bota i traktują ciszę jak zgodę. Wyjście: zapisz w zasadach review, że review przez AI jest doradcze, zachowaj nazwisko zatwierdzającego przy każdym merge’u i awansuj powtarzające się uwagi do reguł, żeby bot miał mniej do znalezienia.