AGENTS.md (Codex, Cursor i inni)¶
Zweryfikowano 2026-09-25
Ręcznie na Linuksie: init, sekcja, którą zapisuje, i inwards check --format json. Żadnego agenta nie uruchomiono. Na macOS (arm64) i Windows CI uruchamia init ze skompilowanym plikiem binarnym przed każdym wydaniem; ręczne sprawdzenie na tych systemach wciąż jest do zrobienia.
Wielu agentów kodujących czyta instrukcje projektu z pliku AGENTS.md w katalogu głównym repozytorium, między innymi OpenAI Codex, Cursor i Jules. Inwards dodaje tam krótką sekcję, która każe agentowi uruchomić sprawdzenie przed zakończeniem pracy i mówi, jak czytać wynik.
Konfiguracja¶
- Zainstaluj Inwards tak, żeby
inwardsbyło wPATHu każdego, kto uruchamia agenta, i dodaj[tool.inwards]dopyproject.toml. -
W projekcie uruchom:
Polecenie przypina
required-versioni domyślną listęignorew[tool.inwards], dodaje.inwards/do.gitignorei dopisuje doAGENTS.mdtę sekcję (tworząc plik, jeśli trzeba):<!-- inwards:begin --> ## Architecture check (Inwards) The layers in `[tool.inwards]` (pyproject.toml) are enforced. Before you finish a task, run: inwards check --format json Exit code 1 means an import breaks a layer: follow the numbered `fix.steps` in the output. Don't edit `[tool.inwards]` to make the check pass; ask the user instead. <!-- inwards:end -->Jeśli projekt uruchamia Inwards przez uv (zależność deweloperska, bez aktywowanego virtualenva), dodaj
--launcher "uv run", a sekcja podauv run inwards check --format json:Sekcja podaje jedno polecenie dla projektu. Uruchomione w katalogu głównym workspace'u uv, sprawdza każdego członka, który ma własne
[tool.inwards](Monorepo i workspace'y uv). Przy kilku tabelach w innym układzie dopisz ręcznie, poza znacznikami, po linii na konfigurację, z--config <package>/pyproject.toml. -
Zacommituj
AGENTS.md. Ponowne uruchomienieinitzastępuje tylko tekst między znacznikami.
Opis architektury (opcjonalny)¶
Sekcja powyżej każe agentowi sprawdzić pracę po fakcie. Opis architektury (brief) podaje mu architekturę, zanim napisze pierwszy import: naprawa naruszenia kosztuje ponowną próbę, uniknięcie go nic nie kosztuje. Domyślnie jest wyłączony, żeby zespół mógł porównać przebiegi z nim i bez niego; inaczej wskaźnik naruszeń z run logu mieszałby jedno z drugim.
inwards context wypisuje opis dla najbliższej tabeli [tool.inwards]:
Wymienia warstwy od najbardziej wewnętrznej, z tym, co każda może importować, miejsce, w którym leżą porty, a jeśli są skonfigurowane, także reguły bibliotek (INW005), konteksty z ich publicznymi modułami i zależnościami (INW002, INW003) oraz reguły opt-in włączone w konfiguracji. Reguły wyłączone w [tool.inwards.rules] są pomijane. Warstwa, która ma pod sobą więcej niż trzy warstwy, podaje je numerami z listy (may import layers 1-9); brief, który tak robi, wymienia grupy warstw sąsiednich raz, w pierwszym zdaniu (sibling groups: 2-4; 5-6), a nie w każdej linii. Publiczne moduły kontekstu leżące w jego jedynym module dzielą ten prefiks (`app.posts.{router,service}`), podobnie jak moduły wpisu deny o wspólnym rodzicu (`app.*.{models,service}`). Gdy INW002 jest wyłączona, konteksty nie podają swoich zależności. Dla przykładowej konfiguracji repozytorium (examples/clean-app), z dziewięcioma warstwami, trzema kontekstami i dwiema regułami opt-in, ma 1662 znaków, około 416 tokenów. Test pilnuje, żeby każdy preset mieścił się poniżej 300 tokenów (przy czterech znakach na token), poza presetem fastapi, który z jedenastoma warstwami, wpisem deny i jedenastoma regułami opt-in ma się zmieścić poniżej 550 (ma około 540), presetem vertical-slices, który z pięcioma rolami wycinka i INW021 ma się zmieścić poniżej 350 (z --scaffold ma około 334), a przykład poniżej 450. Ze znacznikami, które dodaje --write, wygląda tak:
<!-- inwards-brief:begin -->
## Architecture brief (Inwards)
`[tool.inwards]` in pyproject.toml enforces these layers, innermost first. Imports point inwards: never import a layer listed after your own, nor a sibling of yours (sibling groups: 4-6).
1. slice.models (`shop.*.models`): imports no other layer
2. slice.ports (`shop.*.ports`): may import slice.models
3. slice.services (`shop.*.services`): may import slice.models, slice.ports
4. slice.commands (`shop.*.commands`): may import slice.models, slice.ports, slice.services
5. slice.queries (`shop.*.queries`): may import slice.models, slice.ports, slice.services
6. slice.adapters (`shop.*.adapters`): may import slice.models, slice.ports, slice.services
7. slice.api (`shop.*.api`): may import layers 1-6
8. slice.factory (`shop.*.factory`): may import layers 1-7
9. bootstrap (`shop.main`): may import every other layer
Ports: when an inner layer needs something from an outer one, declare a `typing.Protocol` in the inner layer and implement it in the outer layer.
Libraries (INW005):
- slice.models: no web frameworks, database or network clients, `subprocess` or `socket`
Contexts (INW002, INW003): import another context only when yours depends on it, and only its public modules.
- catalog (`shop.catalog`): public `shop.catalog.{api,factory}`; depends on shared
- orders (`shop.orders`): public `shop.orders.{api,factory}`; depends on shared
- shared (`shop.shared`): public `shop.shared`; no dependencies
Opt-in rules:
- INW014: `shop.*.ports` hold only ABCs and Protocols whose methods have no body
- INW015: only `shop.*.factory`, `shop.main` may import or build `shop.*.adapters`; elsewhere take it through a port
<!-- inwards-brief:end -->
Porty to pakiet albo moduł ports leżący bezpośrednio w module warstwy na dysku, na przykład shop/domain/ports.py dla warstwy shop.domain. Warstwa zapisana selektorem (shop.*.models) nie ma jednego takiego modułu, więc brief mówi wtedy „the inner layer”, jak tutaj; moduły portów wymienia linia INW014. Dla konfiguracji zapisanej przez inwards init --style opis podaje też nazwę presetu i jego moduł portów (app.application.ports dla clean i hexagonal), zanim --scaffold go utworzy; preset rozpoznaje po komentarzu, który init umieszcza w tabeli, więc usunięcie tego komentarza usuwa też nazwę.
Każda reguła opt-in włączona w konfiguracji (INW012 do INW016, INW020 i reguły FAPI, przez extend-select, select albo regułę dla roli) dodaje jedną linię pod „Opt-in rules:”. Linia mówi, co robić przy pisaniu kodu, i podaje kluczowe ustawienie reguły: delegate-to dla INW012, moduły portów dla INW014 (jej modules, a bez nich każdy moduł ports), role i allowed-in dla INW015, schemat nazw dla INW016, wymagane metadane dla FAPI001, codes dla FAPI002 i entrypoints dla FAPI003. Reguła zawężona przez modules je wymienia (INW013 in app.*.router). INW015 bez role niczego nie sprawdza, więc wtedy nie dostaje linii. Po inwards init --style fastapi wyglądają tak:
Opt-in rules:
- INW012: endpoints stay thin: call into `domain.service`; no queries or outgoing calls
- INW013: no blocking calls in `async def`: use async clients or a plain `def`
- INW016 in `app.*.models`: singular snake_case tables; datetime/date columns end in `_at`/`_date`
- FAPI001: path operations need a summary or docstring, a response model, `status_code` on POST/DELETE and `description` per `responses` entry
- FAPI002: declare every 4xx a path operation raises in `responses`
- FAPI003: include every `APIRouter` in an app; no include cycles
- FAPI005: declare `/items/me` before `/items/{id}`, so no route is shadowed
- FAPI006: startup and shutdown go in a lifespan, not `on_event`
- FAPI007: a dependency with `yield` re-raises what it catches
- FAPI008: each `operation_id` is unique within its app
- FAPI009: pass `Depends(get_db)`, not `Depends(get_db())`
Żeby trzymać go w AGENTS.md, między własnymi znacznikami obok sekcji sprawdzenia:
inwards context --writedodaje sekcję albo zastępuje w miejscu tekst między<!-- inwards-brief:begin -->i<!-- inwards-brief:end -->. Uruchom je ponownie po zmianie[tool.inwards]; gdy nic się nie zmieniło, odpowiada „up to date”.inwards init --agent agents-md --briefzapisuje obie sekcje w jednym uruchomieniu.--briefdziała z każdym--agent, z--stylei samodzielnie (inwards init --brief).
Opis trafia zawsze do AGENTS.md, nigdy do CLAUDE.md: jeden plik obsługuje wszystkich agentów, a repozytorium, w którym CLAUDE.md to jedna linia @AGENTS.md (jak w tym), daje Claude Code ten sam tekst. Jeśli twój CLAUDE.md nie ma takiej linii, dodaj ją, bo inaczej Claude Code nie zobaczy opisu.
Sprawdź, czy działa¶
- Uruchom sam
inwards check --format json. Licznikviolationsrówny 0 w podsumowaniu oznacza, że projekt jest czysty. - Poproś agenta o zmianę w warstwie domeny. Na koniec powinien uruchomić
inwards check --format jsoni podać wynik.
Rozwiązywanie problemów¶
| Objaw | Przyczyna i rozwiązanie |
|---|---|
| Agent nigdy nie uruchamia sprawdzenia | To instrukcja, a nie hook, więc agent może ją pominąć. Użyj agenta z hookami (takiego jak Claude Code) albo uruchamiaj inwards check w CI. |
init zatrzymuje się z komunikatem „unmatched markers” |
AGENTS.md nie ma dokładnie jednego znacznika inwards:begin i jednego inwards:end (albo, dla opisu architektury, jednego inwards-brief:begin i jednego inwards-brief:end). Popraw je albo usuń ręcznie, a potem uruchom init ponownie. |
inwards: command not found w powłoce agenta |
Środowisko agenta nie widzi pliku binarnego. Dodaj go tam do PATH albo, gdy Inwards jest zależnością deweloperską uv, uruchom ponownie init z --launcher "uv run". (Edycja polecenia między znacznikami działa do następnego init, który przywraca sekcję). |
Twardą blokadę daje inwards check w CI: kod wyjścia 1 oblewa zadanie. Przewodnik po GitHub Actions zawiera workflow, który dodatkowo dodaje adnotacje do pull requestu i wysyła SARIF do code scanning.