5 sierpnia 2026
Ralph — agentic coding workflow od HITL do pracy AFK w sandboksie
- Claude Code
- AI
- Python
Napisałem CLI do czyszczenia feedów produktowych i prawie go nie napisałem. Osiem zadań, 537 linii kodu, 79 testów, mypy --strict bez błędów — tyle dowiozła sama pętla. Kodu nie napisałem ani linijki. Napisałem listę zadań i przeczytałem każdy diff, a ta druga część okazała się trudniejsza, niż zakładałem.
To nie jest tekst o tym, że AI pisze kod — to już wiadomo. To tekst o tym, jak zbudować pętlę, którą da się zostawić bez nadzoru, i po czym poznać, że już można. Bo najciekawsze nie było to, co poszło dobrze, tylko miejsca, w których pętla potrzebowała człowieka — i jedno, w którym agent opisał własną pracę niezgodnie z tym, co faktycznie zrobił.
Ralph — agentic coding workflow bez mistyki
Ralph to autonomiczna pętla kodowania, która sprowadza się do jednego zdania: ten sam prompt uruchamiany w kółko. Agent czyta listę zadań, wybiera pierwsze nieukończone, implementuje TYLKO je, odpala testy, commituje, dopisuje notatkę o tym, co zrobił. Koniec iteracji. Następne uruchomienie startuje od zera — nowy proces, czysty kontekst, ta sama instrukcja.
Cała mechanika opiera się na trzech plikach:
PRD.json— lista zadań. Każde madescription,files(listyincludeiexclude),stop_condition,edge_casesi flagępasses: false.progress.txt— pamięć między iteracjami. Agent dopisuje tu, co zrobił i jakie decyzje podjął. Wersjonowany w repo, bo historia decyzji z pętli jest warta tyle co kod.CLAUDE.md— zasady projektu i oczekiwana jakość kodu.
Sedno: ty definiujesz cel, agent sam do niego dochodzi. Nie piszesz nowego promptu do każdej fazy projektu. Piszesz jeden i pilnujesz, żeby lista zadań była na tyle precyzyjna, że agent nie ma pola do improwizacji.
Cały agentic coding workflow sprowadza się do tego jednego przesunięcia: z pisania promptów do projektowania celu.
Klucz jest w stop_condition. Nie „działa dobrze”, tylko: uv run pytest zielony, uv run mypy src bez błędów, uv run ruff check . bez błędów. Warunek mierzalny, nie ocena — bo agent, który sam ocenia, czy skończył, zwykle uznaje, że skończył.
Najpierw HITL, dopiero potem AFK
Dwa tryby, w tej kolejności — nigdy odwrotnie.
HITL (Human In The Loop) to ralph-once.sh: jedna iteracja, sesja interaktywna, siedzisz i patrzysz. Skrypt to dosłownie jedna komenda:
claude --permission-mode acceptEdits \
"@PRD.json @progress.txt @CLAUDE.md \
1. Przeczytaj PRD.json, progress.txt i CLAUDE.md. \
2. Znajdź pierwsze zadanie z passes: false \
i zaimplementuj TYLKO je — nie ruszaj plików \
wykluczonych ani należących do innych zadań. \
..."
Składnia @plik wczytuje zawartość od razu na start rozmowy — agent od pierwszej sekundy widzi listę zadań i pamięć z poprzednich iteracji, zamiast dochodzić do tego narzędziami.
AFK (Away From Keyboard) to afk-ralph.sh: wiele iteracji bez nadzoru, w sandboksie, z twardym limitem. Odpalasz ./afk-ralph.sh 5 i wracasz po kawę.
Przejście z pierwszego na drugi nie jest formalnością. HITL to moment, w którym uczysz się, jak pętla zachowuje się przy niejasnym zadaniu — zanim oddasz jej stery na pięć zadań z rzędu. Ryzyko, przed którym ostrzegają praktycy tej metody, to agent po cichu zawężający zakres i ogłaszający zwycięstwo. U mnie wyszło inaczej i ciekawiej: agent zgłaszał problemy uczciwie, ale raz udokumentował własną naprawę niezgodnie z tym, co zrobił. Zielony commit nie pokaże ani jednego, ani drugiego.
Gdzie pętla potrzebowała człowieka
Dwie rzeczy złapane w trakcie, jedna ustawiona, zanim pętla w ogóle ruszyła.
Luka w architekturze, zgłoszona przez agenta. Po zadaniu 2 Ralph napisał w progress.txt, że nie ma czym sprawdzić samego SKU: model Pydantic wymaga poprawnej ceny i ilości, więc nie da się go skonstruować tylko po to, żeby zwalidować jedno pole, gdy inne jest jednocześnie zepsute. Zgłosił to uczciwie — i zaproponował lokalny, jednowierszowy helper wewnątrz classify.py.
Odrzuciłem tę propozycję. Zamiast duplikatu reguły rozszerzyłem zakres zadania 3 o parse_sku i parse_name w parsing.py, symetryczne do istniejących parse_price i parse_quantity. Pętla bez nadzoru poszłaby wariantem agenta — bo zadanie 3 formalnie dałoby się zamknąć na zielono z regułą wklejoną w dwóch miejscach. To nie jest błąd agenta. To jest decyzja architektoniczna, której nikt mu nie oddelegował.
Sprzeczność w moim własnym PRD. Opis zadania 3 mówił „reasons odrzuconego wiersza = suma powodów z pól odrzuconych”. Jego własny edge_case mówił co innego: wiersz z jednym polem odrzuconym i jednym naprawionym ma zawierać oba powody. Agent wybrał wariant z edge_case i miał rację, ale konsekwencja wyszła dopiero przy zadaniu 6: naiwne kubełkowanie powodów po statusie wiersza wpuściłoby powód typu „naprawiono” (price_format) do licznika odrzuceń, gdy tylko trafi w tym samym wierszu na niezwiązane odrzucenie. Raport przestałby się zgadzać z danymi.
Doprecyzowałem PRD dla zadania 6, zanim report.py w ogóle powstał. To jest realna praca człowieka w tym workflow: nie pisanie kodu, tylko rozstrzyganie dwuznaczności, zanim agent utrwali jedną z nich w trzech plikach.
Ground truth, którego agent nie dotknął. Tego akurat HITL nie złapał — to decyzja sprzed pętli. Dane testowe wygenerowałem deterministycznym skryptem przed jej startem i celowo nie zrobiłem z tego zadania dla Ralpha. Generator zapisuje feed.csv razem z feed_manifest.json — plikiem, który mówi dokładnie, ile błędów każdego typu wstrzyknął. Dzięki temu porównuję raport wyprodukowany przez CLI z liczbami, których agent nigdy nie widział.
Jeśli agent pisze kod i pisze do niego testy, to sprawdzanie go tymi testami jest bez sensu. Potrzebujesz punktu odniesienia spoza pętli. Ten sam problem rozwiązywałem inaczej przy agencie na Claude Agent SDK — tam rolę bramki pełniły jawne sign-offy w PRD.
AFK w sandboksie — sbx, nie Docker Desktop
Tu warto uciąć nieporozumienie, bo sam je miałem. Sandbox obsługuje sbx — samodzielny CLI od Dockera, który nie wymaga Docker Desktop. Uruchamia własny proces sandboxd tworzący izolowane mikro-VM pod agentów AI. Żadnego Dockerfile, żadnego docker compose.
brew trust docker/tap
brew install docker/tap/sbx
sbx login
sbx run claude .
Trzy rzeczy, które trzeba wiedzieć, zanim to odpalisz:
Polityka sieciowa. Przy pierwszym logowaniu sbx pyta o domyślną politykę: Open, Balanced albo Locked Down. Wybrałem Balanced — pętla potrzebuje PyPI (uv sync) i GitHuba (git push), ale nie ma powodu, żeby agent bez nadzoru miał nieograniczony dostęp do sieci. Cały sens sandboksa to ograniczenie szkód, więc otwieranie go „na wszelki wypadek” znosi powód, dla którego go stawiasz.
Logowanie. sbx run claude . odpala świeżą, niezalogowaną instancję Claude Code — jak po nowej instalacji. Zwykłe /login subskrypcją Pro/Max działa i rozliczanie zostaje subskrypcyjne, mimo że dokumentacja sugerowała drogę przez klucz API. Sekrety (np. token GitHuba) wstrzykuje się przez sbx secret set -g github, bez lądowania tokenu jako plik w kontenerze. Sandbox uruchomiony przed ustawieniem sekretu trzeba zrestartować, żeby go podłapał.
Dostęp do plików. Domyślnie bind mount read-write: katalog projektu jest wpięty do sandboksa jak okienko na prawdziwy folder, zmiany widać na hoście natychmiast jako zwykły git diff. Alternatywa --clone daje agentowi prywatny klon repo, a commity wracają przez osobny remote. Zostałem przy bind mount — prościej, a limit iteracji i tak jest twardym hamulcem.
Sam skrypt pętli to trzydzieści kilka linii, z czego połowa to prompt. Reszta bez magii:
result=$(sbx exec -w "$WORKDIR" "$SANDBOX" \
claude -p --dangerously-skip-permissions "..." < /dev/null)
echo "$result"
if [[ "$result" == *"<promise>COMPLETE</promise>"* ]]; then
exit 0
fi
Tryb -p (print) sprawia, że agent kończy proces sam, zamiast zostawiać otwartą sesję — konieczne, bo nikt nie siedzi, żeby ją zamknąć. < /dev/null mówi „nie czekaj na wejście”. Sygnał <promise>COMPLETE</promise> agent wypisuje, gdy wszystkie zadania mają passes: true — wtedy pętla kończy się od razu, zamiast przepalać kolejne iteracje na sprawdzanie, że nie ma nic do zrobienia.
Pułapka: CLAUDE.md zablokował własną pętlę
Najlepsza lekcja z całego projektu, bo nie znalazłem jej w żadnej dokumentacji.
Pierwsze uruchomienie AFK stanęło. Model prosił o zgodę na zapis pliku — mimo że sandbox sbx startuje Claude Code w trybie bypass permissions, i mimo że po drugiej stronie nie było nikogo, kto mógłby odpowiedzieć.
Winny był mój własny CLAUDE.md. Opisałem w nim kulturę HITL: „po każdej iteracji: przegląd diffa i commita, zanim uruchomię kolejną”, „decyzja architektoniczna, którą trzeba rozstrzygnąć pod nadzorem”. Pisałem to o trybie interaktywnym. Model przeczytał to jako regułę projektu i zastosował do wszystkiego, łącznie z sesją bezobsługową. Domyślny bypass sandboksa mówił jedno, instrukcja projektu drugie — wygrała instrukcja.
Naprawa była trzyczęściowa i każda część była potrzebna. W CLAUDE.md dopisałem punkt, który jawnie zawęża opis nadzoru do ralph-once.sh. W promptcie AFK dodałem zdanie wprost: to sesja bezobsługowa, nikt nie odpowie, masz przyznane uprawnienia, nie pytaj o zgodę. I dopiero wtedy dołożyłem jawną flagę --dangerously-skip-permissions, której skrypt wcześniej w ogóle nie podawał — polegałem na domyślnym trybie sandboksa.
Jest jeszcze druga warstwa tej historii i to ona jest ciekawsza. Komunikat commita naprawczego napisał model. Napisał go sprzecznie z własnym diffem: twierdzi, że pętla zacinała się „mimo flagi bypass”, podczas gdy ten sam diff pokazuje, że flagę dopiero dodaje. Gdybym pisał ten artykuł z historii gita zamiast z diffów, powtórzyłbym to zdanie jako fakt. Agent zrobił dobrą naprawę i opisał ją źle — a opis został w repo na stałe, wyglądając wiarygodnie.
Wnioski dwa. Pierwszy: model nie wie, pod którym skryptem właśnie działa. Jedynym sygnałem jest treść promptu. Jeśli CLAUDE.md opisuje kulturę pracy, ta kultura obowiązuje wszędzie — także tam, gdzie miała nie obowiązywać. Ta sama dyscyplina co przy bazie wiedzy utrzymywanej przez agenta: plik instrukcji to nie notatka dla ludzi, to konfiguracja wykonywalna.
Drugi: komunikat commita napisany przez agenta to jego wersja zdarzeń, nie ich zapis. Zapisem jest diff. Ten pierwszy czyta się szybciej i właśnie dlatego jest groźniejszy.
Co z tego wyszło
Osiem zadań, wszystkie passes: true. Na wyjściu z pętli: 79 testów zielonych, mypy --strict czysty, ruff czysty, raport jakości danych zgodny z manifestem co do sztuki. Ta ostatnia bramka była w stop condition od początku i celowo nie dało się jej zautomatyzować — porównanie z ground truth robiłem ręcznie.
Potem puściłem jeszcze jeden przebieg agentem, już poza pętlą — audyt, który ruszył kod produkcyjny w ośmiu plikach — rename statusu CLEAN na OK, wyprowadzenie schematu Parquet wprost z modelu Pydantic zamiast czterech gołych asercji, obsługa plików w kodowaniu innym niż UTF-8. Stan dzisiejszy to 82 testy i 557 linii. Piszę o tym, bo „pętla dowiozła projekt” i „projekt był gotowy” to dwa różne zdania, a mieszanie ich to najłatwiejszy sposób na sprzedanie tej metody drożej, niż jest warta.
Narzędzie robi to, co miało: wczytuje brudny feed CSV, klasyfikuje każdy rekord jako OK / REPAIRED / REJECTED, naprawia to, co da się naprawić bezpiecznie ("29,99 zł" → 29.99, ujemna ilość → wartość bezwzględna), odrzuca resztę z nazwanym powodem i eksportuje do Parquet plus report.json z rozbiciem na powody.
Pięć rzeczy, które powiedziałbym sobie na starcie:
- Czas idzie w PRD, nie w kod. Godzina na doprecyzowanie
edge_casesjest tańsza niż iteracja, w której agent zgaduje. Sprzeczność w PRD jest droższa niż brak PRD. - Feedback loops jako bramka, zero wyjątków. „Nie commituj czerwonego stanu” musi być w promptcie, nie w domyśle. I tak weryfikuj samodzielnie po pętli — agent bywa w błędzie co do tego, czy naprawdę wszystko przeszło.
- Ground truth spoza pętli. Jeśli agent pisze i kod, i testy, potrzebujesz punktu odniesienia, którego nie dotknął.
- Czytaj diffy, nie komunikaty commitów. Opis napisany przez agenta bywa niezgodny z tym, co agent naprawdę zmienił — a wygląda wiarygodnie i zostaje w repo na zawsze.
CLAUDE.mdczyta każda sesja, także ta, o której nie myślałeś. Zasady pisz z jawnym zakresem obowiązywania.
Czy to szybsze niż napisanie tego CLI ręcznie? Dla tej skali — porównywalnie. Różnica jest w tym, gdzie idzie czas: nie w pisanie kodu, tylko w projektowanie zadania i rozstrzyganie dwuznaczności, zanim agent utrwali złą odpowiedź w trzech plikach naraz.
I tu jest haczyk, którego nie widać na starcie. Dobre PRD wymaga wiedzieć z góry, jak rozłożyć projekt architektonicznie — które decyzje są nieodwracalne, co musi zostać rozstrzygnięte, zanim powstanie pierwszy plik, gdzie przebiegają granice między modułami. Pętla tego z ciebie nie zdejmuje. Przesuwa wysiłek wcześniej i wyżej, tam gdzie pomyłka kosztuje więcej. W tym projekcie moje PRD wymagało poprawki w locie dwa razy na osiem zadań — przy zakresie, który znam dobrze, i przy narzędziu, które sam wybrałem. To nie jest biegłość, którą się łapie po trzeciej pętli.