Przestań płacić tokenami za grepa

Nawet przy drobnych zmianach szybko zużywałem limit Claude. Samo uruchomienie grepa nie było kosztowne. Tokeny pochłaniało przekazywanie jego wyników do rozmowy. Część 1 serii o tym, jak lokalny skrypt może odciążyć model w rutynowych zadaniach.

Oct 23, 2026~10 min czytania
Przestań płacić tokenami za grepa

Chciałem zrobić więcej w ramach tego samego limitu

We wrześniu dodawałem do swojego portfolio pojedynczy element Three.js z pomocą Claude Code. Zmiana była niewielka, a wykorzystanie limitu szybko rosło.

Po kolejnych zatwierdzeniach widziałem na wskaźniku skoki rzędu 10 do 15 punktów procentowych. To była obserwacja z codziennej pracy, nie wynik kontrolowanego pomiaru. Wystarczyła jednak, żebym zaczął przyglądać się temu, jak pracuję z narzędziem.

Chciałem kończyć więcej zadań w ramach limitu, który już miałem. Coraz bardziej irytowało mnie, że kończył się akurat wtedy, gdy zostawały jeszcze poprawki i sprawdzenie zmian.

Zacząłem więc patrzeć na wszystko, co działo się wokół samej edycji kodu. Agent szukał plików, czytał źródła, uruchamiał polecenia i przekazywał ich wyniki do rozmowy. Część tych informacji pomagała mu podejmować decyzje. Część służyła tylko do obsługi procesu albo zawierała znacznie więcej szczegółów, niż wymagał kolejny krok.

Czy mógłbym zrobić więcej lokalnie, zanim poproszę model o interpretację wyników?

Z tego pytania powstał /runtask.

Zostawić modelowi mniej pracy

Pomysł był prosty: powtarzalne operacje przekazać skryptowi, a modelowi zostawić zrozumienie zadania, wybór podejścia i zmiany w kodzie.

Grep już działa lokalnie. Tak samo kompilator i linter. Samo uruchomienie tych poleceń nie zużywa tokenów modelu. Zużywa je natomiast zlecanie modelowi, żeby koordynował ich wywołania i interpretował wyniki. Właśnie tutaj widziałem miejsce na zmianę: więcej tej obsługi wykonać lokalnie i ustalić, jaka część wyników rzeczywiście musi trafić do rozmowy.

Na przykład skrypt może zapisać początkowy wynik linta, uruchomić go ponownie po zmianach i porównać oba wyniki. Model zajmie się wtedy tylko nowymi komunikatami, zamiast czytać pełne logi i samodzielnie ustalać, co się zmieniło.

/runtask rozrósł się do skilla dla Claude Code, za którym stoi około 61 KB kodu w Node bez zewnętrznych zależności, w dwóch plikach: rt.mjs i core.mjs. Skrypt przygotowuje kontekst, uruchamia kontrole, śledzi zmiany i zapisuje postęp.

Praca jest podzielona na trzy części:

CzęśćOdpowiedzialność
Główna rozmowaKoordynowanie zadania i decyzje, które wymagają mojego udziału
Lokalny skrypt w NodeWyszukiwanie, przygotowanie raportów, porównywanie wyników i zapisywanie stanu
AgenciPlanowanie i wprowadzanie zmian, opcjonalnie code review lub pomoc w wyszukiwaniu

Główna rozmowa i agenci zużywają tokeny. Sam kod wykonywany przez Node ich nie zużywa, choć poproszenie modelu o wywołanie skryptu i zinterpretowanie wyniku już tak.

Ograniczyłem liczbę agentów. Jeden agent na Sonnet 5.5 planuje i wykonuje zadanie. Drugi, również na Sonnet 5.5, może opcjonalnie zrobić code review. Agent na Haiku pomaga w wyszukiwaniu, kiedy lokalne polecenia nie wystarczają. Ten sam agent przechodzi od planu do implementacji, więc korzysta z kontekstu, który już poznał, bez kolejnego przekazywania zadania.

Zrezygnowałem w tym procesie z Opusa, bo we wrześniu widziałem, jak szybko rosło przy nim wykorzystanie limitu. Chciałem sprawdzić, ile uda mi się zrobić z Sonnetem i lepiej uporządkowanym procesem. Sonnet czasem potrzebuje więcej kontekstu albo dodatkowej tury. Dlatego warto porównywać cały nakład pracy potrzebny do zakończenia zadania, razem z poprawkami. Wybór mniejszego modelu jest tylko jednym z elementów.

Zacząć od właściwego kodu, potem sięgać dalej

Agent zaczyna od dwóch plików: manifestu opisującego repozytorium oraz pakietu z zadaniem i wybranymi fragmentami kodu.

Manifest ma limit 6000 znaków. Zawiera informacje o stacku, dostępnych kontrolach i plikach, które mogą być istotne dla zadania. Pakiet kontekstu ma limit 18 000 znaków i zawiera fragmenty kodu, importy oraz pliki, które bezpośrednio od nich zależą. To limity materiału na start, a nie wszystkiego, co agent przeczyta w trakcie pracy.

Skrypt wybiera pliki, dopasowując opis zadania do ich ścieżek i zawartości. Słowo znalezione w nazwie pliku dostaje więcej punktów niż to samo słowo w jego treści. Jeśli opis jest zbyt ogólny, pomaga podanie konkretnych ścieżek.

Potem agent może sięgać po kolejne informacje za pomocą lokalnych poleceń:

PolecenieCo zwraca
RT find "<page text>"Pliki znalezione po ścieżce, po tekście tłumaczeń i18n lub po tekstach w kodzie
RT symbol <name>Miejsce deklaracji symbolu
RT refs <name>Użycia w maksymalnie 20 plikach, z oznaczeniem plików importujących symbol
RT slice <file> --lines A:BWskazany zakres linii, z długimi liniami przyciętymi do 400 znaków
RT deltaZmiany od początku zadania, które pomagają ustalić, co sprawdzić dalej
RT check <file>Wyniki kontroli dla wskazanego pliku

Najbardziej lubię RT find, bo pozwala szukać po tekście, który użytkownik widzi na stronie. Opis zadania często odnosi się do etykiety przycisku albo nagłówka. W aplikacji z tłumaczeniami skrypt może powiązać ten tekst z kluczem i18n, a następnie znaleźć kod, który z niego korzysta.

Agent nadal przeszukuje repozytorium. Różnica polega na tym, że zaczyna od zawężonych wyników i w razie potrzeby szuka dalej. Grep i Glob pozostają dostępne jako rozwiązanie awaryjne. Jeśli i one nie doprowadzą do właściwego kodu, może pomóc agent wyszukujący.

Fragmenty kodu mają wyraźną adnotację, że pokazują tylko część obrazu. To ważne: w krótkim wycinku może zabraknąć warunku, który tłumaczy zachowanie komponentu. Ograniczanie kontekstu ma sens tylko wtedy, gdy agent nadal ma dość informacji, żeby poprawnie wprowadzić zmianę.

Log o rozmiarze 58 KB nie zawsze musi trafić do kontekstu

Wyniki kontroli były jednym z najbardziej oczywistych miejsc, w których dało się ograniczyć ilość tekstu przekazywanego modelowi.

Przed edycją skrypt zapisuje punkt odniesienia, czyli baseline: wyniki sprawdzania typów, linta, testów i opcjonalnie buildu. Po zmianach porównuje nowe wyniki z początkowymi i wyróżnia błędy, które pojawiły się później. Zlicza też powtarzające się komunikaty, zamiast traktować każdą unikalną treść jako jeden element. Pomaga to wykryć wzrost liczby takich samych błędów, choć nie pozwala rozpoznać każdego nowego problemu, jeśli stare i nowe komunikaty wyglądają identycznie. Istniejące błędy nadal mają znaczenie, ale nie powinny po cichu zamieniać drobnego zadania w porządkowanie całego projektu.

Jedno z zapisanych uruchomień pokazuje skalę różnicy:

PlikRozmiar
Surowy log linta z baseline'u58 451 bajtów
Podsumowanie weryfikacji w verify.md184 bajty

Te dane pochodzą z różnych etapów tego samego zadania i mają różny poziom szczegółowości. Pokazują różnicę między pełnym logiem a krótkim raportem. Nie są pomiarem oszczędności tokenów ani kosztu wykonania całego zadania.

Agent zwykle czyta podsumowanie. Jeśli to nie wystarcza do zrozumienia problemu, może otworzyć szczegółowy wynik. Prompt caching może obniżać koszt ponownego przetwarzania tego samego kontekstu, więc sam rozmiar raportów nie mówi mi, ile limitu Pro zaoszczędziłem. Pokazuje jednak, że wyniki rutynowych kontroli da się przekazać znacznie krócej.

PASS, FAIL i INCOMPLETE

Weryfikacja kończy się jednym z trzech wyników: PASS, FAIL albo INCOMPLETE.

INCOMPLETE oznacza, że nie udało się ukończyć wymaganej weryfikacji. Któraś z wymaganych kontroli mogła się nie uruchomić albo wcześniejszy błąd testów uniemożliwił uzyskanie jednoznacznego wyniku. Pozostałe kontrole mogły przy tym zakończyć się poprawnie i dostarczyć przydatnych informacji.

Trzeba odróżnić kontrolę opcjonalną, której nie zlecono, od wymaganej, której nie udało się wykonać. Raport powinien pokazywać, co było wymagane, co zakończyło się powodzeniem, a co pominięto lub zablokowano. Pominięta kontrola nigdy nie jest dowodem na to, że kod ją przeszedł.

Właśnie dlatego ten status ma znaczenie. Komunikat „brak nowych błędów” w sytuacji, gdy linter w ogóle nie wystartował, ukrywałby problem, który powinienem sprawdzić. INCOMPLETE jasno sygnalizuje brak pełnej weryfikacji.

Wrócę do niego w drugiej części.

Kontynuować pracę bez odtwarzania całego przebiegu

Skrypt zapisuje na dysku również stan zadania: co przeskanowano, na jakie zmiany pozwala plan, które kroki odnotowano i jakie były wyniki kontroli. Standardowo zadanie przechodzi od scanned, przez ready, planned, building i verified, aż do finished. Jeśli nie można kontynuować pracy, dostaje stan blocked.

Polecenia skryptu sprawdzają, czy dane przejście jest dozwolone, i zapisują nowy stan. Agent ma korzystać z tych poleceń, zamiast bezpośrednio edytować plik ze stanem zadania.

Migawka oparta na hashach zapisuje początkowy stan plików w repozytorium. Dzięki temu skrypt wykrywa zmiany wprowadzone podczas zadania, nawet jeśli w katalogu roboczym były już wcześniej niezacommitowane zmiany. Plan wymienia pliki do edycji, utworzenia lub usunięcia. Zapisane kopie oryginałów służą później do porównania zmian podczas code review, bez potrzeby tworzenia commita.

Jeżeli trzeba zmienić kolejny plik, agent ma się zatrzymać i poprosić mnie o rozszerzenie zatwierdzonego zakresu. Weryfikacja porównuje rzeczywiste zmiany z tym zakresem i wskazuje rozbieżności między planem a wykonaniem.

Hooki odpowiadają za formatowanie i kontrole po operacjach Edit/Write. Hook sprawdzający kod skupia się na nowych komunikatach względem baseline'u. Problemy z uruchomieniem samej kontroli również muszą być widoczne, żeby nie pomylić ich z pustą listą błędów.

Wybrane operacje Gita są blokowane przez hooki i ustawienia uprawnień. Dzięki temu mniej zależy od tego, czy model pamięta instrukcje. Każde z tych zabezpieczeń obejmuje jednak tylko te działania, które rzeczywiście sprawdza.

Zapisany stan przydaje się także wtedy, gdy praca zostaje przerwana. W kolejnej sesji można odczytać postęp i kontynuować od tego miejsca. Model nadal potrzebuje kontekstu, ale ma już konkretny punkt wyjścia.

Ustaliłem też ograniczenia dla agenta: maksymalnie dwie próby poprawek oraz krótki raport końcowy z wynikiem, wykonanymi kontrolami i pozostałymi ograniczeniami. Chciałem, żeby problemy stawały się widoczne, zanim seria kolejnych prób pochłonie resztę limitu.

Krótki raport nadal musi mówić prawdę

Sprowadzenie całej tej pracy do kilku linijek ma swoje ryzyko. Uspokajający status może przesłonić to, jak niewiele faktycznie sprawdzono.

W jednym z uruchomień z 21 października sprawdzanie typów i lint zakończyły się poprawnie. Testy pominięto, ponieważ nie wykryto konfiguracji Vitest ani Jest. Build nie został uruchomiony. Te szczegóły są ważne niezależnie od końcowego statusu.

Agent stwierdził też, że kolor tekstu zapewnia kontrast co najmniej 4,5:1 w obu motywach. Skrypt nie zmierzył jednak tego współczynnika. Zapisanie takiej deklaracji jako dowodu spełnienia wymagania nie oznaczało, że cokolwiek sprawdzono.

To dobrze pokazuje granice tego podejścia. Skrypt może odnotować, co uruchomiono, i podsumować wynik. Zachowanie interfejsu, jego wygląd oraz wymagania, które trzeba sprawdzić w przeglądarce, nadal potrzebują odpowiedniego potwierdzenia. Opcjonalny reviewer czytający diff nie dostarcza go automatycznie.

Chodzi o raport na tyle krótki, żeby był użyteczny, i na tyle precyzyjny, żeby było widać, czego jeszcze nie sprawdzono.

Potem zajrzałem do archiwum

Na tym etapie całość miała dla mnie sens. Zawęzić początkowy kontekst, skrócić wyniki rutynowych operacji, korzystać z jednego agenta i zapisywać postęp poza rozmową. Wszystko po to, żeby większa część limitu zostawała na decyzje i zmiany, które przybliżają mnie do ukończenia zadania.

Wziąłem więc 12 wcześniejszych uruchomień z archiwum i policzyłem, ile z nich dotarło do stanu finished.

Zero.

I tu zaczyna się druga część.

*Zbudowane, pomierzone i napisane przeze mnie. Zredagowane z pomocą Claude’a.

Czy ten wpis był pomocny?

Jak lepiej wykorzystać limit Claude Code dzięki lokalnemu skryptowi | Code Nomad