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.

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.
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 rozmowa | Koordynowanie zadania i decyzje, które wymagają mojego udziału |
| Lokalny skrypt w Node | Wyszukiwanie, przygotowanie raportów, porównywanie wyników i zapisywanie stanu |
| Agenci | Planowanie 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.
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ń:
| Polecenie | Co 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:B | Wskazany zakres linii, z długimi liniami przyciętymi do 400 znaków |
RT delta | Zmiany 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ę.
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:
| Plik | Rozmiar |
|---|---|
| Surowy log linta z baseline'u | 58 451 bajtów |
Podsumowanie weryfikacji w verify.md | 184 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.
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.
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.
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.
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?