Whilly — bootstrap a second machine
Полный runbook для онбординга вторичной машины (далее — machine B) в работающий Whilly v4.1 setup. Цель: заставить B claim’ить задачи из того же Postgres-плана, что и primary workstation (далее — machine A), без коллизий, без рассинхрона статусов и без двух разных Claude API-ключей. Эта страница — глубокая версия
Continuing-On-Another-Machine.md; если ты ищешь TL;DR, читай тот документ.
TL;DR
# на machine B, с нуля
git clone git@github.com:mshegolev/whilly-orchestrator.git
cd whilly-orchestrator
python3.12 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
# Postgres reach — выбери ОДИН вариант (см. секцию 4)
ssh -fN -L 5432:127.0.0.1:5432 user@machine-a # ← простейший: SSH-tunnel
# Username и DB по умолчанию — `whilly` (из docker-compose.yml на machine A).
# Password — твой rotated secret; вынеси в env var, чтобы не светить в shell-history:
read -s PG_PASSWORD && export PG_PASSWORD
export WHILLY_DATABASE_URL=$(printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" 127.0.0.1 5432 whilly)
# Claude (если нужен proxy — см. Whilly-Claude-Proxy-Guide)
claude --version
# Smoke + claim одной задачи
whilly plan show "Whilly v4.1 — Out-of-scope items from v4.0 release"
whilly run --plan "Whilly v4.1 — Out-of-scope items from v4.0 release" --max-iterations 1
Если все 7 команд отработали без ошибок — machine B готов к параллельной работе с A. Если упало — мотай к секции Troubleshooting.
1. Когда этот документ нужен
| Сценарий | Читай это |
|---|---|
| «У меня одна машина, хочу впервые поднять Whilly» | Getting-Started.md |
| «Я уже знаю Whilly, переключаюсь на другую машину, не хочу всё перечитывать» | Continuing-On-Another-Machine.md (cheat-sheet) |
| «Нужен production-grade onboarding второй машины с разбором всех вариантов сети, безопасности и troubleshoot’а» | этот документ |
| «Хочу запускать Whilly через корпоративный proxy» | Whilly-Claude-Proxy-Guide.md (TASK-109) |
Если у тебя только doc’и / тесты / code-review без orchestrator’а — секции 4-8 можно пропустить: для этого достаточно git pull и редактора.
2. Prerequisites
На machine B должно быть:
- Python 3.10+ (3.12 предпочтительнее — CI matrix). Проверь:
python3 --version. - git + SSH-ключ, прописанный в GitHub.
- claude CLI на
PATH— если планируешь запускать worker (а не только править docs). Установка:npm install -g @anthropic-ai/claude-code(или per platform binary). Аутентификация:claude login. - Один из трёх механизмов сетевого доступа к Postgres на machine A — см. секцию 4.
Опционально, в зависимости от выбранного Postgres-режима:
- Docker Desktop / docker.io — для standalone-режима (4.1).
- SSH access на machine A — для tunnel-режима (4.2).
- Tailscale / WireGuard на обеих машинах — для mesh-режима (4.3).
3. Установка
# 1. Клонируем репо
git clone git@github.com:mshegolev/whilly-orchestrator.git
cd whilly-orchestrator
git checkout main
git pull --ff-only
# 2. Виртуальное окружение
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
# 3. Editable install + dev зависимости (alembic, pytest, ruff, asyncpg, ...)
pip install -e '.[dev]'
# 4. Sanity-check
whilly --help # CLI dispatcher
whilly-worker --help # standalone remote-worker entry (TASK-022c)
ruff check whilly/ tests/ # должно сказать "All checks passed!"
Если whilly --help ругается на отсутствующий бинарь — pyenv shim не подхватил venv. Самый надёжный fallback — звать через интерпретатор: python -m whilly --help.
4. Postgres reach — выбери один режим
Это главное решение бутстрапа. От него зависит, шарится ли план между A и B (и значит работаете ли вы над одной задачей или над двумя независимыми).
| # | Режим | Setup на B | Сетевая зависимость | Shared plan? |
|---|---|---|---|---|
| 4.1 | Standalone (local docker) | ./scripts/db-up.sh + alembic upgrade head + whilly plan import .planning/v4-1_tasks.json | нет | нет — у B свой Postgres, отдельный мир |
| 4.2 | SSH reverse-tunnel к A | ssh -fN -L 5432:127.0.0.1:5432 user@A | A должен быть достижим по SSH | да |
| 4.3 | Tailscale / WireGuard mesh | static internal IP к A’s Postgres | mesh-сеть на обеих машинах | да |
| 4.4 | scripts/whilly-share.sh (TASK-111) | одной командой; пока не реализовано | публично-туннелируемый control plane | да |
4.1 Standalone (local Docker)
Подходит для: изолированной разработки, plane-без-сети, или экспериментов где не хочется пачкать общий план.
# 1. Запускаем Postgres контейнер. Скрипт идемпотентен и ждёт healthcheck.
./scripts/db-up.sh
# 2. Применяем миграции
alembic upgrade head
# 3. Импортируем v4.1 план в локальный Postgres
whilly plan import .planning/v4-1_tasks.json
После этого у B свой полный 31-task граф. Изменения статусов не долетают до A — это две отдельные базы. Подходит как fallback или offline-режим, но не удовлетворяет acceptance criteria TASK-110 (“shared plan with primary”).
4.2 SSH reverse-tunnel к primary
Самый простой способ получить shared plan. Не требует расшаривания Postgres-порта в публичную сеть — A просто открывает SSH-доступ для B (типичная конфигурация для laptop ↔ VPS / dev-server).
На machine B:
# Forward A:5432 → localhost:5432 на B. -fN: fork и не запускать команду на удалённом хосте.
ssh -fN -L 5432:127.0.0.1:5432 user@machine-a
# Проверка туннеля: psql или pg_isready
pg_isready -h 127.0.0.1 -p 5432 # должно сказать "accepting connections"
Если уже занят локальный 5432 (своим Postgres’ом — например, после 4.1) — выбери другой порт:
ssh -fN -L 15432:127.0.0.1:5432 user@machine-a
export WHILLY_DATABASE_URL=$(printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" 127.0.0.1 15432 whilly)
Туннель умирает вместе с SSH-сессией. Чтобы он переживал sleep/wake/loss-of-net — оборачивай через autossh:
brew install autossh # macOS
autossh -M 0 -fN -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" \
-L 5432:127.0.0.1:5432 user@machine-a
4.3 Tailscale / WireGuard mesh
Подходит для: постоянной двунаправленной связности (laptop работает из дома и кафе, machine A — на офисном столе или в облаке), когда SSH-tunnel становится утомительным к каждому wake.
Tailscale-flow (для WireGuard аналогично, замени имена):
# На обеих машинах:
brew install tailscale && sudo tailscale up # macOS
# или: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up
# На A: разрешить Tailscale-IP'ам доступ к Postgres'у. По умолчанию docker-compose
# биндит 127.0.0.1:5432; нужно либо bind 0.0.0.0:5432 в Tailscale-only network,
# либо открыть Postgres только для tailscale0 интерфейса через pg_hba + listen_addresses.
# Минимальная правка docker-compose.yml на A:
# ports:
# - "100.64.0.0/10:5432:5432" # подсеть Tailscale CGNAT
# На B: используй Tailscale-имя или IP A
export WHILLY_DATABASE_URL=$(printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" machine-a.tail-XXXX.ts.net 5432 whilly)
⚠ Open-Postgres-в-mesh — это всё ещё credentials, прописанные в DSN. Дефолт из docker-compose.yml (username whilly, password — тот же) подходит только для local-dev; не открывай Postgres в mesh-сеть с дефолтным паролем. См. Security caveats.
4.4 Future: scripts/whilly-share.sh (TASK-111)
Когда TASK-111 ландится, секцию 4.2 / 4.3 можно будет заменить на одну команду:
export SHARE_URL=https://example.lhr.rocks # placeholder — published share URL
# (placeholder — недоступно на момент TASK-110)
./scripts/whilly-share.sh --serve # на A
./scripts/whilly-share.sh --connect "$SHARE_URL" # на B
Скрипт инкапсулирует cloudflared / ngrok-туннель к control plane, в обход прямой Postgres-экспозиции. Когда landed — обнови этот раздел и удали 4.2/4.3 как «legacy options».
5. WHILLY_DATABASE_URL recipe per mode
Все DSN ниже используют bash-substitution
${PG_PASSWORD}— установи переменную один раз и она подхватится во всехexport-командах. Безопасный способ ввода (не пишет в shell-history):read -s PG_PASSWORD && export PG_PASSWORD. Альтернатива — keyring-вариант ниже.
Username и DB name по умолчанию —
whilly(значения изdocker-compose.yml:POSTGRES_USER/POSTGRES_DBdefaults). Password в local-dev тожеwhilly— но для shared-mode setups (4.2 / 4.3) обязательно ротируй на сильный random secret до открытия Postgres’а в сеть. См. Security caveats.
| Режим | WHILLY_DATABASE_URL |
|---|---|
| 4.1 Standalone | printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" 127.0.0.1 5432 whilly |
| 4.2 SSH-tunnel (default port) | printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" 127.0.0.1 5432 whilly |
| 4.2 SSH-tunnel (alt port) | printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" 127.0.0.1 15432 whilly |
| 4.3 Tailscale (hostname) | printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" machine-a.tail-XXXX.ts.net 5432 whilly |
| 4.3 Tailscale (CGNAT IP) | printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" 100.x.y.z 5432 whilly |
Подставь свой пароль вместо <PG_PASSWORD>. Положи итог в ~/.zshrc / ~/.bashrc / ~/.config/fish/conf.d/whilly.fish или в проект-локальный .envrc (если используешь direnv). Не коммить в git — пароль в DSN.
Для постоянной работы безопаснее вынести password в keyring и собирать DSN на лету:
# Один раз — записать password в OS keyring (он не уйдёт в git/историю shell'а):
python3 -c "import keyring, getpass; keyring.set_password('whilly', 'pg_password', getpass.getpass('PG password: '))"
# В shell-init собирать DSN из keyring:
PG_PASSWORD=$(python3 -c "import keyring; print(keyring.get_password('whilly','pg_password'))")
export WHILLY_DATABASE_URL=$(printf 'postgresql://%s:%s@%s:%s/%s' whilly "$PG_PASSWORD" 127.0.0.1 5432 whilly)
6. Claude CLI bootstrap
Anthropic credentials — per-machine, не передаются через git. Аутентифицируйся отдельно:
claude login # один раз на B
claude --version # sanity
Если B стоит за корпоративным proxy и не достучится до api.anthropic.com напрямую — поднимай SSH-туннель к гейту с outbound и натравливай Whilly на него. Полный гайд — Whilly-Claude-Proxy-Guide.md. Минимум:
ssh -fN -L 11112:127.0.0.1:8888 gpt-proxy.internal
export WHILLY_CLAUDE_PROXY_URL=http://127.0.0.1:11112
Эти env vars влияют только на дочерний Claude-процесс; asyncpg/httpx Whilly остаются direct (см. TASK-109).
7. Smoke tests
# 1. План видим из B (тот же 31-task граф что на A)
whilly plan show "Whilly v4.1 — Out-of-scope items from v4.0 release"
# Ожидаемо: список задач TASK-101 … TASK-111 со статусами как на primary
# 2. Claim ровно одной готовой задачи и завершение worker-цикла
whilly run --plan "Whilly v4.1 — Out-of-scope items from v4.0 release" --max-iterations 1
# Ожидаемо: registered worker=<hostB>-<uuid>, обработана одна pending-task,
# процесс вышел с кодом 0
Если задача успешно дошла до done или failed (терминальный статус) — на A в whilly plan show ты увидишь её новый статус. Это и есть критерий «B claim’ает из общего плана».
Note: Описание TASK-110 упоминает
whilly worker local --once— это устаревшее имя из ранних драфтов. Актуальная команда —whilly run --plan <id> --max-iterations 1. Флаг--onceсуществует только в standalone-бинарникеwhilly-worker(remote-worker для control-plane’а; см.whilly-worker --help).
8. Avoiding collisions with the primary worker
Whilly v4 спроектирован так, чтобы N независимых worker’ов могли одновременно тянуть задачи из одного плана:
- Каждый worker регистрируется со своим
worker_id(по умолчанию<hostname>-<8-hex-uuid>— два worker’а на разных машинах никогда не столкнутся PK’ом). - Claim’ят через
SELECT … FOR UPDATE SKIP LOCKED— два worker’а никогда не возьмут одну и ту же строку. - Heartbeat — независимый per-worker; зомби-worker (потерял сеть, упал ноут) детектится по lease-timeout, его задача возвращается в pool.
Что нельзя делать на B:
- Передавать
--worker-id <fixed-id>, совпадающий с A’s id. Это нарушит invariant и поломает heartbeat’ы обеим сторонам. - Запускать
whilly plan importвторой раз поверх существующегоplan_id— упадёт на foreign-key constraint. Если действительно нужно пересоздать — сначалаwhilly plan reset(когда TASK-103 ландится) или вручную:DELETE FROM events WHERE task_id IN (SELECT id FROM tasks WHERE plan_id='…'); DELETE FROM tasks WHERE plan_id='…'; DELETE FROM plans WHERE id='…';. - Заводить долгоживущий
whilly run(без--max-iterations) на ноуте, который уходит в sleep — heartbeat прекратится, A пометит worker’а stale, lease перейдёт обратно в pool, B при wake-up попробует завершить чужую с её точки зрения задачу. Используй--max-iterations Nили останавливай worker’а до закрытия крышки.
Архитектурный фон — docs/Whilly-v4-Worker-Protocol.md (claim semantics, heartbeat, lease timeouts) и whilly/cli/run.py (флаги).
9. Sync auto-memory (optional)
~/.claude/projects/<project-id>/memory/MEMORY.md живёт на машине, на которой Claude был запущен. Между A и B оно не ходит. Если хочется continuity:
- Quick path — в первом промпте на B вставь 2-3 последних значимых memory-line из A.
- Long-term — держи
~/.claude/projects/<project-id>/memory/в приватном git-репо или sync через Dropbox/iCloud/rsync.
CLAUDE.md (per-project, в корне репо) уже в git — синхронизируется автоматически с git pull.
10. Security caveats
| Риск | Mitigation |
|---|---|
| Shared bearer tokens между worker’ами (per-worker rotation — TASK-101, пока pending). | Не пиши план/токены в публичные репо; ограничь сетевую expose Postgres’а до lo / Tailscale-only. |
| Anonymous tunnels (cloudflared / ngrok без auth — релевантно когда TASK-111 ландится). | Включай auth-headers, привязывай к стабильным subdomain’ам с allowlist; не оставляй endpoint открытым на ночь. |
Postgres credential rotation. Дефолтные POSTGRES_USER / POSTGRES_PASSWORD (из docker-compose.yml, оба whilly) подходят только для local docker; в любой shared-mode конфигурации меняй password на длинный random secret, храни в keyring, ротируй ежеквартально. | docker compose down -v && POSTGRES_PASSWORD=$(openssl rand -hex 32) docker compose up -d; обнови WHILLY_DATABASE_URL на A и B одновременно. |
Claude proxy leak — если HTTPS_PROXY экспортирован в shell, его подхватит httpx parent-процесса Whilly и пошлёт control-plane запросы через тот же tunnel. | Используй WHILLY_CLAUDE_PROXY_URL (изолирован к спавну Claude) вместо глобального HTTPS_PROXY. См. TASK-109. |
whilly init мутирует os.environ на CLI-флагах (TASK-109 follow-up). Не критично для secret’ов, но создавай новый shell для запусков с разными --claude-proxy. | Документировано в коде; не делай implicit assumptions, явно указывай флаги. |
11. Troubleshooting
whilly run: WHILLY_DATABASE_URL is not set → Не выставлен env var. Перепроверь echo $WHILLY_DATABASE_URL.
asyncpg.exceptions.InvalidPasswordError или connection refused → Туннель не поднят / упал. Перепроверь pg_isready -h 127.0.0.1 -p 5432. Для SSH-tunnel убедись ps aux | grep ssh | grep 5432.
whilly plan show 'Whilly v4.1 — …' возвращает plan not found → Ты в standalone-режиме (4.1) и не сделал whilly plan import .planning/v4-1_tasks.json, или подключился не к той Postgres-инстанции. Проверь psql $WHILLY_DATABASE_URL -c 'SELECT id FROM plans;'.
whilly run claim’ает task, но Claude падает с connection refused к api.anthropic.com → Сеть B не достучится напрямую до Anthropic. Настрой proxy через TASK-109 (WHILLY_CLAUDE_PROXY_URL).
На primary внезапно worker stale-сообщения → B ушёл в sleep с активным worker’ом. Останавливай worker до закрытия крышки или используй --max-iterations.
whilly --help зависает / печатает мусор → pyenv shim путает venv. Пробуй python -m whilly --help. Если помогло — добавь .venv/bin в начало PATH или используй direnv.
12. Related docs
Continuing-On-Another-Machine.md— короткий cheat-sheet для частого переключения между машинами.Getting-Started.md— первая установка Whilly с нуля (single-machine).Whilly-Init-Guide.md—whilly initflow для новых планов.Whilly-Claude-Proxy-Guide.md— TASK-109, Claude через HTTPS-proxy.Whilly-v4-Worker-Protocol.md— claim semantics, heartbeat, bearer token lifecycle.Whilly-Usage.md— полный CLI / env-var reference.scripts/db-up.sh— idempotent local Postgres bootstrap.whilly/cli/run.py,whilly/cli/worker.py— local + remote worker entrypoints..planning/v4-1_tasks.json— канонический task graph для v4.1.