tmux dla flot agentów: sesje, send-keys, wait-for
Każdy poradnik „tmux + agenty AI” napisany w ostatnim roku zaczyna się tak samo: pętla wołająca git worktree add, potem tmux new-window na każdy worktree, potem send-keys, żeby odpalić agenta. Ta rada jest dziś o jedną warstwę za nisko. Claude Code sam tworzy i sprząta swoje worktree przez --worktree, Cursor ma /worktree w oknie Agents, a Codex prowadzi wątki w worktree natywnie. Problem izolacji, który rozwiązywały skrypty tmuxowe, wchłonęły same agenty.
Do czego więc nadal jest tmux? Do trzech rzeczy, których żaden agent za ciebie nie zrobi: nie widzisz sześciu agentów naraz, klapa laptopa się zamyka, a agenty nie umieją ze sobą rozmawiać. To są problemy multipleksera terminala i tmux radzi sobie z nimi bardzo dobrze. Ten artykuł jest o tym szwie — nie o git worktree add w pętli.
Czego się nauczysz
Dział zatytułowany „Czego się nauczysz”- Jasnego podziału zadań: co należy do agenta, co do tmuxa, a czego nie robi nikt
- Skryptu
bin/fleet, który rozdziela N zadań bez ręcznego tworzenia ani jednego worktree send-keysjako prawdziwego kanału agent-agent, razem z dwiema flagami, bez których psuje ci wiadomościcapture-paneiwait-forjako pollingu i bariery — jak agent-orkiestrator nadzoruje workerów- Hooków agenta podłączonych do tmuxa, żeby „ten agent jest zablokowany” było dokładnym sygnałem, a nie zgadywaniem z ekranu
Kto co posiada
Dział zatytułowany „Kto co posiada”| Zagadnienie | Właściciel | Dlaczego nie ten drugi |
|---|---|---|
| Izolacja plików | Agent | claude --worktree, /worktree w Cursorze, wątki worktree w Codeksie. Skryptowanie tego samemu dubluje dziś wspieraną funkcję |
| Izolacja runtime’u (porty, baza dev, nazwy Dockera) | Ty | Nikt tego za ciebie nie zrobi. Worktree izoluje pliki i nic więcej |
| Widoczność (który agent mnie teraz potrzebuje) | tmux | Okna, status line, choose-tree. Agent wie tylko o sobie |
| Trwałość (zamknij klapę, wróć po SSH) | Serwer tmuxa | Proces agenta umiera razem z terminalem, o ile coś poza nim nie przeżyje |
| Komunikacja między agentami | tmux | send-keys, capture-pane, wait-for. Żaden agent nie ma skrzynki odbiorczej |
Sesja na zadanie, nie panel na zadanie
Dział zatytułowany „Sesja na zadanie, nie panel na zadanie”Instynkt każe dzielić panele. Działa do trzech agentów i rozsypuje się przy sześciu: panele stają się zbyt wąskie, żeby przeczytać diff agenta, i tracisz orientację, który jest który. Sesje skalują się lepiej, bo mają nazwy i skaczesz do nich po nazwie.
Układ, który się broni: jedna sesja tmuxa na zadanie, a w niej jedno okno na agenta, jedno na serwer dev, jedno na testy.
# skok po nazwie, bez liczenia panelitmux switch-client -t agent-auth-refresh
# albo przejrzyj całą flotę jako drzewotmux choose-tree -ZsWrzuć flotę do status line, żeby „który agent mnie potrzebuje” dało się odpowiedzieć bez przełączania się gdziekolwiek. Wystarczą nazwy sesji:
set -g status-left-length 80set -g status-left '#{?client_prefix,#[reverse],}[#S] 'set -g status-right '#(tmux list-sessions -F "#{session_name}#{?session_attached,*,}" | tr "\n" " ")'set -g status-interval 5Bazową konfigurację tmuxa — przemapowanie prefiksu, nawigację po panelach, history-limit pod długie transkrypty agenta — opisuje mistrzostwo terminala. Ten artykuł zakłada, że ta warstwa istnieje, i buduje na niej warstwę floty.
Skrypt fan-out
Dział zatytułowany „Skrypt fan-out”Cały skrypt — i zwróć uwagę, czego w nim nie ma: ani jednej wzmianki o git worktree. Tym zajmuje się agent.
#!/usr/bin/env bash# bin/fleet -- jedna sesja tmuxa na zadanie; agent sam zarządza swoim worktreeset -euo pipefail
[ $# -gt 0 ] || { echo "użycie: bin/fleet <slug> [slug...]" >&2; exit 1; }
for slug in "$@"; do # send-keys wpisuje do shella, więc slug w rodzaju 'x;id' uruchomiłby `id`. # Waliduj, zanim cokolwiek dotrze do panelu. case "$slug" in *[!a-zA-Z0-9._-]*|"") echo "niepoprawny slug: $slug" >&2; exit 1 ;; esac
session="agent-$slug"
if tmux has-session -t "$session" 2>/dev/null; then echo "istnieje, pomijam: $session" >&2 continue fi
# -d odłączona, -c żeby agent startował w katalogu repo tmux new-session -d -s "$session" -c "$PWD" -n agent tmux send-keys -t "$session:agent" "claude --worktree $slug" C-m
# drugie okno na to, co dane zadanie musi uruchamiać tmux new-window -d -t "$session" -n run -c "$PWD"done
first="agent-$1"if [ -n "${TMUX:-}" ]; then tmux switch-client -t "$first"else tmux attach -t "$first"fibin/fleet auth-refresh rate-limit-headers stripe-webhook-idempotencyTrzy sesje, trzy agenty, trzy worktree w .claude/worktrees/, każdy na własnym branchu worktree-<slug>. Nie napisałeś ani jednej komendy gita.
send-keys: kanał agent-agent
Dział zatytułowany „send-keys: kanał agent-agent”send-keys wpisuje tekst do panelu. Agent nie odróżnia tego od tego, że piszesz ty — i właśnie dlatego działa jako kanał komunikacyjny między agentami.
tmux send-keys -t agent-api -l "Migracja z agent-auth-refresh właśnie wylądowała na main. Zrób rebase i puść ponownie testy integracyjne."tmux send-keys -t agent-api C-mDwa szczegóły decydują o tym, czy to działa, czy po cichu psuje wiadomość:
-lwysyła tekst dosłownie. Bez tego tmux parsuje argument jako nazwy klawiszy. Wiadomość zawierającaEnter,Spacealbo;zostaje zinterpretowana, a nie wpisana — i dostajesz pokaleczony prompt albo przypadkową komendę. Do prozy zawsze-l.- Zatwierdzaj osobnym wywołaniem.
C-mto nazwa klawisza, więc nie pojedzie w tym samym argumencie-l. Wyślij tekst, potem wyślij nową linię.
capture-pane: polling
Dział zatytułowany „capture-pane: polling”tmux capture-pane -p -J -t agent-api -S -200-pwypisuje na stdout zamiast do bufora wklejania, więc możesz to przepuścić przez pipe.-Jscala zawinięte linie. Bez tego każda długa ścieżka pliku albo ramka stosu jest rozerwana na szerokości terminala, więc twójgrepjej nie znajdzie. Ta flaga to różnica między wiarygodnym pollingiem a flaky.-S -200zaczyna 200 linii wstecz w scrollbacku, a nie na górze widocznego panelu.
Pętla nadzorcza na tym zbudowana — kształt, którego używa agent-orkiestrator do pilnowania workera:
#!/usr/bin/env bash# bin/agent-watch <sesja> -- zgłoś, kiedy worker potrzebuje człowiekaset -euo pipefailsession="$1"
while tmux has-session -t "$session" 2>/dev/null; do tail=$(tmux capture-pane -p -J -t "$session:agent" -S -40) case "$tail" in *"Do you want"*|*"Allow "*|*"(y/n)"*) echo "BLOCKED $session"; break ;; *"Error"*|*"error:"*|*"FAIL"*) echo "ERROR $session"; break ;; esac sleep 15doneTo jest zgadywanie z ekranu i jest tak kruche, jak wygląda — nowe brzmienie promptu je psuje. Używaj tego jako awaryjnej ścieżki, nie jako mechanizmu. Mechanizmem są hooki, dwie sekcje niżej.
wait-for: bariera
Dział zatytułowany „wait-for: bariera”Polling pali cykle i dodaje opóźnienie. wait-for to prawdziwa bariera: proces czekający blokuje się, dopóki ktoś nie zasygnalizuje kanału.
# w workerze, po tym, co faktycznie miało się udaćtmux wait-for -S migration-applied# w orkiestratorzetmux wait-for migration-appliedtmux send-keys -t agent-api -l "Migracja zaaplikowana. Rebase na main i puść ponownie testy integracyjne."tmux send-keys -t agent-api C-mNazwy kanałów żyją na serwerze tmuxa, więc dowolny panel może zasygnalizować dowolny inny — bez wspólnych plików i bez żonglowania PID-ami. To najczystszy prymityw zależności dostępny flocie: zadanie B czeka na kanale A-done, zadanie A sygnalizuje go, kiedy wyląduje, i nic nie odpytuje.
Hooki: dokładny stan zamiast zgadywania z ekranu
Dział zatytułowany „Hooki: dokładny stan zamiast zgadywania z ekranu”Polling przez capture-pane wydaje się kruchy, bo jest kruchy — wnioskujesz o stanie z wyrenderowanego tekstu. Hooki agenta odwracają to: agent sam mówi tmuxowi, co się stało, w chwili, w której się stało.
Hook Stop zmienia nazwę okna, gdy agent skończy, i sygnalizuje kanał nazwany po własnej sesji, żeby czekający mógł zablokować się na jednym konkretnym workerze. Hook Notification robi to samo dla „potrzebuję zgody”. Oba to jedna linia tmuxa.
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "S=$(tmux display-message -p -t \"$TMUX_PANE\" '#{session_name}' 2>/dev/null); tmux rename-window -t \"$TMUX_PANE\" done 2>/dev/null; [ -n \"$S\" ] && tmux wait-for -S \"done-$S\" 2>/dev/null; true" } ] } ], "Notification": [ { "hooks": [ { "type": "command", "command": "tmux rename-window -t \"$TMUX_PANE\" 'NEEDS-YOU' 2>/dev/null; tmux display-message -d 3000 'agent czeka na zgodę' 2>/dev/null; true" } ] } ] }}Cztery rzeczy warte uwagi. $TMUX_PANE jest ustawione w każdym panelu tmuxa, więc hook celuje w siebie bez wpisywania sesji na stałe. Nazwa kanału jest wyprowadzana z sesji, a nie sztywna — ma to większe znaczenie, niż wygląda, i następny akapit jest o tym dlaczego. 2>/dev/null i kończące true sprawiają, że hook jest nieszkodliwy, gdy agent działa poza tmuxem — inaczej niezerowy kod wyjścia objawia się jako błąd hooka przy każdym zatrzymaniu. A nazwa okna staje się nośnikiem informacji: rzut oka na status line mówi, który agent cię potrzebuje, bez żadnego pollingu.
Kiedy hooki są na miejscu, bin/fleet-deps przestaje skanować ekran i zaczyna czekać na konkretnego workera:
# sesja to agent-db-migration, więc jej hook Stop sygnalizuje done-agent-db-migrationtmux wait-for done-agent-db-migrationProwadzenie każdego z agentów
Dział zatytułowany „Prowadzenie każdego z agentów”claude --worktree <nazwa> tworzy .claude/worktrees/<nazwa>/ na branchu worktree-<nazwa> i tam startuje. --worktree "#1234" odgałęzia się od pull requesta. Przy wyjściu Claude proponuje usunięcie worktree, jeśli jest czysty, i pyta, gdy jest w nim praca.
Dla flot liczą się dwa ustawienia. worktree.baseRef domyślnie ma wartość "fresh" (branch od domyślnej gałęzi na zdalnym); ustaw "head", gdy workery mają budować na twojej niewypchniętej pracy. A .worktreeinclude w katalogu repo kopiuje pliki z .gitignore, np. .env, do każdego nowego worktree.
Dla nieinteraktywnych workerów w panelu claude -p całkowicie pomija sprawdzanie trustu — ale oznacza też brak promptu przy wyjściu, więc nic nie sprząta worktree. Te usuwaj samodzielnie przez git worktree remove.
Codex ma własne wątki w worktree, więc komenda w panelu to po prostu codex, a tryb worktree wybierasz w sesji. Dla workerów, które mają wykonać i wyjść, codex exec "<zadanie>" działa nieinteraktywnie i kończy się — co czysto komponuje się z wait-for:
tmux send-keys -t agent-api -l 'codex exec "dodaj nagłówki rate-limit do każdego route" && tmux wait-for -S api-done'tmux send-keys -t agent-api C-mKażdy worktree niesie własny AGENTS.md, co jest najczystszym sposobem zawężenia workera do jednego wycinka repo — wpisz wąski plik instrukcji do worktree przed promptem.
Okno Agents w Cursorze już prowadzi agenty w lokalnych workspace’ach, worktree, chmurze i po SSH, więc do pracy interaktywnej zwykle nie chcesz tmuxa przed nim.
Tam, gdzie tmux zarabia na siebie przy Cursorze, jest ścieżka headless: cursor-agent w panelu, prowadzony tą samą instalacją send-keys / capture-pane / wait-for co wszystko inne. .cursor/rules per worktree zawęża wtedy każdego workera, bo agent czyta reguły katalogu, w którym wystartował.
Trwałość: cały sens serwera
Dział zatytułowany „Trwałość: cały sens serwera”Agent w środku refaktoru umiera razem z terminalem. Serwer tmuxa nie.
-
Odłączaj się, a nie wychodź.
prefix + d. Agenty pracują dalej.tmux attach -t agent-auth-refreshpodnosi flotę z powrotem, także z innej maszyny po SSH. -
Postaw flotę tam, gdzie może stać. Mały VPS albo zapasowa maszyna z serwerem tmuxa oznaczają, że zamknięcie laptopa nic nie zatrzymuje.
ssh box -t tmux attach -t agent-auth-refreshto cały workflow. -
Przeżyj reboot świadomie.
tmux-resurrectzapisuje i przywraca układ sesji i okien;tmux-continuumzapisuje go cyklicznie. Zauważ, co przywracają: układ, nie rozmowy agentów. Połącz to z resume samego agenta (claude --resume,codex resume), żeby wrócić do prawdziwego stanu. -
Podnieś scrollback, zanim będzie potrzebny.
set -g history-limit 50000. Transkrypt agenta przebija domyślne 2000 linii w jednym refaktorze, acapture-pane -S -200nie przeczyta tego, co tmux już wyrzucił.
Kiedy to się psuje
Dział zatytułowany „Kiedy to się psuje”Oskryptowałeś worktree, którymi agent już zarządza. Objaw to dwa worktree na zadanie albo agent sprzątający katalog, którego twój skrypt się jeszcze spodziewa. Wybierz jednego właściciela. W 2026 tym właścicielem jest agent.
send-keys wylądował w promcie o uprawnienia. Twoja instrukcja odpowiedziała na dialog tak/nie i agent zrobił coś, o co nie prosiłeś. Zawsze capture-pane przed send-keys, a najlepiej prowadź przekazania z hooków, żebyś pisał tylko do agentów, o których wiesz, że są bezczynne.
send-keys pokaleczył wiadomość. Zapomniałeś -l i tmux odczytał Enter albo C-c w twojej prozie jako klawisz. Objawy sięgają od ucięcia promptu do zabicia agenta.
Grepy po capture-pane nie widzą oczywistych trafień. Zapomniałeś -J, więc zawinięta ścieżka albo ramka stosu jest rozerwana na szerokości terminala. Dodaj -J i ten sam grep zaczyna działać.
wait-for wisi bez końca. Nikt nie zasygnalizował kanału — zwykle bo worker padł przed linią sygnalizującą. Sygnalizuj na obu ścieżkach (cmd && tmux wait-for -S ok || tmux wait-for -S failed) i rozróżniaj je po stronie czekającego, albo owiń czekanie w timeout.
Scrollback zjadł dowody. Domyślny history-limit to 2000 linii. Podnieś go do 50000 przed długim biegiem, nie po.
Sześć paneli nie przyspieszyło cię. tmux czyni fan-out tanim, co czyni łatwym przekroczenie własnego pasma review. Sufitem jest to, ile diffów potrafisz realnie ocenić — kalibracja w równoległości zespołowej, a wpływ na zużycie tokenów w optymalizacji kosztów.
Flotę zatrzymały limity, nie tmux. Każdy panel wydaje przydział tego samego konta. Agent, który „zawisł”, to często agent, który uderzył w limit. Sprawdź panel, zanim zaczniesz debugować skrypt.