Skip to content

Repository files navigation

PromptPilot

Background task queue for AI CLIs — schedule prompts, retry on rate limits, manage everything via Web UI or Telegram bot.

Works with Claude Code, OpenAI Codex, Qwen Code, Cursor Agent, or any CLI that accepts a prompt argument.


Универсальный планировщик промптов для AI CLI — очередь, планирование и автоматический retry.

Работает с любым AI CLI: Claude Code, Codex, Qwen Code и другими.

Очередь задач PromptPilot

Одна очередь для разовых задач, расписаний и автономных workflow.

Возможности

  • Мульти-провайдер — Claude, Codex, Qwen, Cursor Agent, или любой свой CLI
  • Очередь задач с приоритетами (1 — высший, 10 — низший)
  • Планирование — запуск промптов в заданное время
  • Выбор модели — для Claude Code провайдеров (sonnet/opus/haiku) и любых провайдеров с явным списком models (Web UI + бот)
  • Rate limit detection — автоматическое определение лимитов API; перегрузка провайдера (529 overloaded) отличается от исчерпанной квоты и так и называется в уведомлении
  • Exponential backoff — retry с нарастающей задержкой (60s → 1h)
  • Crash recovery — при перезапуске воркера зависшие задачи возвращаются в очередь
  • CLI + Web UI — два интерфейса на выбор
  • Telegram бот — управление задачами через Telegram с авторизацией по номеру телефона
  • Пароль на создание задач в боте — опциональная защита через PP_TASK_PASSWORD
  • Автономный режим — флаг на задачу для запуска Claude или Codex без интерактивных подтверждений
  • Уведомления — Telegram бот присылает сообщение как только задача завершилась (с результатом или ошибкой)
  • Интеграция с herdr — задачи выполняются в живых терминальных сессиях: permission-диалог не роняет задачу, а ждёт подтверждения (с уведомлением в Telegram); режим keep_pane оставляет сессию открытой для продолжения
  • herdr → Telegram мост — уведомления о заблокированных/завершённых агентах herdr с кнопками «Подтвердить / Экран / Ответить»
  • Опциональная авторизация Web UI/API — токен через PP_API_TOKEN
  • Скилы Claude Code — запуск /skill-name через Web UI и бота (для всех Claude Code провайдеров)
  • Продолжение сессии — кнопка 💬 в боте после завершённой задачи для диалога в той же сессии
  • Пауза воркера — кнопка ⏸ в Web UI и боте, чтобы временно остановить обработку без потери задач
  • Автоперезапуск worker'а — при обновлении кода пакета worker сам перезапускается между задачами
  • Повторяющиеся задачи — поле Recur: 6h, 30m, daily@09:00 — новая задача создаётся автоматически, в том числе после неудачного прогона
  • Экран расписания — кнопка 🗓 в Web UI: все повторяющиеся задачи как серии — период, следующий запуск, исход прошлого и пометка «оборвана»
  • Диагностика внешних конвейеров — локальные профили показывают backlog, возраст, churn, ETA и проектные health-check без запуска LLM и расхода токенов
  • Правка задачи в очереди — провайдер, модель, эффорт, повтор и приоритет меняются прямо в карточке, без пересоздания задачи
  • Фоновый запуск (detached) — запустить процесс в фоне и сразу завершить задачу (для серверов, ботов, polling-скриптов)
  • Per-task таймаут — индивидуальный лимит времени задачи в Web UI (переопределяет глобальный PP_TASK_TIMEOUT)
  • Свой git worktree на задачу — галка «🌿 свой worktree»: агент работает в отдельном чекауте на ветке pp/t<id>, твоё рабочее дерево не трогается, результат виден как diff
  • Срыв среды ≠ провал задачи — отказ доступа (401/403/5xx) или обрыв ответа возвращает задачу в очередь вместо failed
  • Сторож запретов — PreToolUse-хук режет катастрофические команды у задач с --dangerously-skip-permissions: диалога там нет, значит запрет должен держать не он
  • Параллельные задачиPP_CONCURRENCY=N: worker выполняет несколько задач одновременно, но никогда две в одном рабочем дереве
  • Отмена running-задач — из Web UI, бота или API: worker убивает процесс задачи (группу процессов) в течение пары секунд
  • Уведомления об обновлениях — баннер в Web UI когда выходит новая версия
  • Дашборд стоимости — статистика расходов за сегодня / неделю / всего по провайдерам
  • Дописать решателю — приписка к задаче: пара фраз, которые уйдут в следующий прогон (в том числе в повтор после rate limit)
  • Итог задачиPP_VERDICT=1: агент заканчивает строкой ИТОГ: ГОТОВО | НУЖЕН ЧЕЛОВЕК | НЕ СМОГ | …, и уведомление сразу говорит, надо ли идти смотреть; тихий итог ПУСТО («делать нечего») в Telegram не шлётся — для повторяющихся дежурных задач
  • Расход за окно лимитаpp usage: сколько сожжено за последние 5 ч по ВСЕМ сессиям Claude Code, включая herdr-задачи и живую переписку
  • Tray-приложение — двойной клик на pp.exe, иконка в трее, всё управление мышью
  • Standalone .exe — сборка без зависимостей через PyInstaller
  • SQLite — данные хранятся локально в ~/.promptpilot/
  • Workflow Orchestrator (W3) — planner разбивает большую задачу на утверждаемые этапы, затем автономный цикл «исполнитель → deterministic gate → независимый аудитор» ведёт каждый этап до PASS; есть crash recovery, лимиты, provenance и JSON/Markdown-экспорт для последующего анализа

Workflow Orchestrator

Архитектура автономного цикла описана в docs/WORKFLOW_ORCHESTRATOR_SPEC.md. W0–W3 реализованы: можно импортировать прежние раунды, поручить сильному planner разбиение новой задачи, отредактировать и один раз утвердить план, после чего PromptPilot автоматически передаёт ход между executor, gate и reviewer и сохраняет непрерывный журнал. Настройка описана в docs/WORKFLOW_AUTOMATION_GUIDE.md.

В Web UI кнопка WorkflowsНовый workflow открывает четырёхшаговый мастер: задача, команда агентов, правила автоматизации и preflight. Перед созданием он без выполнения проектного кода проверяет Git-путь, имя ветки, доступность providers и синтаксис gate-команд. Есть рекомендуемый, экономный и усиленный профили, а повторяющиеся настройки можно сохранить как локальный шаблон.

Workflow PromptPilot: сейчас работает исполнитель

Экран сразу показывает, у кого «мяч» и какой переход произойдёт дальше.

Как устроен автономный цикл

Цикл planner, executor, gate и независимого аудитора

Минимальный ручной пилот:

pp workflow create workflow.json
pp workflow import-history <id-or-slug> history.json
pp workflow start <id-or-slug> --base-sha <sha>
pp workflow dispatch <id-or-slug> executor --file executor-prompt.md
pp workflow sync <id-or-slug>
pp workflow gate <id-or-slug> PASS --gate-id tests
pp workflow dispatch <id-or-slug> reviewer --file reviewer-prompt.md
pp workflow sync <id-or-slug>
pp workflow review <id-or-slug> PASS --summary "Принято"

pp workflow list
pp workflow show <id-or-slug>
pp workflow rounds <id-or-slug>
pp workflow events <id-or-slug> --json
pp workflow findings <id-or-slug>

Формат переноса старых итераций и правила отделения проверенных фактов от ручных воспоминаний описаны в docs/HISTORY_IMPORT_GUIDE.md.

Установка

Вариант 1 — Скачать готовый .exe (Windows)

  1. Скачай последний релиз: github.com/ivanarama/PromptPilot/releases
  2. Распакуй архив PromptPilot-vX.X.X-windows.zip в любую папку
  3. Заполни .env (шаблон уже в архиве)
  4. Запусти start.ps1 или pp.exe tray

Вариант 2 — Из исходников (Python 3.10+)

git clone https://github.com/ivanarama/PromptPilot.git
cd PromptPilot
pip install -e .

Или установка из pip-пакета (из релиза):

pip install promptpilot-X.X.X-py3-none-any.whl

Требования: Python 3.10+, хотя бы один AI CLI в PATH (claude, codex, qwen и т.д.).

Вариант 3 — Docker (Linux-сервер)

git clone https://github.com/ivanarama/PromptPilot.git
cd PromptPilot
docker compose up -d --build
# Web UI: http://localhost:8420   (по умолчанию только loopback)

docker compose поднимает два сервиса — server и worker — с общей SQLite БД на volume promptpilot-data (данные переживают пересборку). Образ уже включает Node.js и @anthropic-ai/claude-code и работает под non-root пользователем.

  • Аутентификация Claude в контейнере: проще всего через LiteLLM-прокси — задайте ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN в .env рядом с docker-compose.yml. Альтернатива для обычной подписки — примонтировать свой логин: добавьте в сервисы volumes: - ~/.claude:/home/pp/.claude.
  • Рабочие репозитории: чтобы задачи работали по вашим проектам, примонтируйте их в контейнер worker (пример в docker-compose.yml) и задайте PP_PROJECTS_ROOT.
  • Безопасность: порт публикуется только на 127.0.0.1:8420. Чтобы открыть в сеть — поменяйте на "8420:8420" и задайте PP_API_TOKEN (тогда /api/* требуют Authorization: Bearer <token>), либо ставьте за reverse-proxy.
  • Telegram-бот выключен по умолчанию: раскомментируйте сервис bot в docker-compose.yml и задайте PP_TG_TOKEN (при необходимости PP_TG_PROXY).
  • Ограничение: Docker-вариант локальный и одномашинный — herdr и раздача задач по ssh на другие машины из контейнера не работают.

Вариант 4 — Standalone-бинарь под Linux (без Python)

./build.sh          # → dist/pp
./dist/pp server

Использует тот же кроссплатформенный pp.spec, что и Windows. Tray-зависимости (pystray/Pillow) в Linux-сборку не входят — pp tray там просто сообщит об этом и предложит pp worker/pp server/pp bot.

Быстрый старт

# Добавить задачу
pp add "Объясни что такое рекурсия"

# Запустить воркер (выполняет задачи)
pp worker

# В другом терминале — запустить веб-интерфейс
pp server
# UI доступен на http://127.0.0.1:8420 (браузер не открывается автоматически)

Важно: pp worker и pp server — два отдельных процесса, которым нужно работать одновременно. Пока воркер не запущен, задачи просто висят в pending.

Запуск: два режима

Режим 1 — Tray (рекомендуется для .exe)

Двойной клик на pp.exe — иконка появляется в системном трее, worker и server стартуют автоматически.

Правый клик на иконке:

▶ Worker          ← кликнуть = остановить
▶ Server          ← кликнуть = остановить
■ Bot             ← кликнуть = запустить (нужен PP_TG_TOKEN в .env)
─────────────────
Запустить все
Остановить все
─────────────────
Открыть Web UI    ← открывает браузер на http://127.0.0.1:8420
─────────────────
Выход             ← останавливает все сервисы и закрывает трей

Цвет иконки показывает состояние: 🟢 все работают / 🟠 частично / ⚫ остановлено.

Или явно через команду:

pp tray

Режим 2 — CLI (все команды работают)

pp worker          # запустить воркер
pp server          # запустить веб-UI
pp bot             # запустить Telegram бот
pp add "промпт"    # добавить задачу
pp list            # список задач
# и т.д.

Оба режима работают с одной и той же БД и настройками.

Файл .env (настройки)

Все настройки — токен бота, путь к claude.exe, разрешённые номера — хранятся в .env файле.

Скопируй шаблон и заполни:

copy .env.example .env
notepad .env

.env рядом с pp.exe (или рядом со скриптом):

PP_TG_TOKEN=7123456789:AAF...
PP_TG_ALLOWED_PHONES=+79001234567,+79007654321
PP_CLAUDE_EXE=C:\Users\YourName\.local\bin\claude.exe
PP_DEFAULT_CLI=claude

Авторизация Claude: PromptPilot запускает claude.exe как обычный процесс — он наследует окружение текущего пользователя. Достаточно один раз выполнить claude auth login на этой машине, больше ничего настраивать не нужно.

Порядок поиска .env:

  1. Рядом с pp.exe — для дистрибуции
  2. Текущая рабочая директория — для разработки
  3. ~/.promptpilot/.env — постоянный пользовательский конфиг

Значения из .env применяются только если переменная не задана в окружении — то есть $env:PP_TG_TOKEN всегда перекрывает .env.

PowerShell: запуск одной командой

Запустить воркер + сервер в фоне:

.\start.ps1

Запустить всё включая Telegram бота:

$env:PP_TG_TOKEN = "ваш-токен"
$env:PP_TG_ALLOWED_PHONES = "+79001234567"
.\start.ps1 -Bot

Логи пишутся в .\logs\. Остановить:

.\stop.ps1

Скрипт автоматически использует dist\pp.exe если он собран, иначе pp из PATH.

Сборка .exe

Сборка standalone-бинаря (не требует Python на целевой машине):

.\build.ps1

На выходе: dist\pp.exe. Использование аналогично:

.\dist\pp.exe worker
.\dist\pp.exe server
.\dist\pp.exe bot
.\dist\pp.exe add "промпт"

Примечание: при первом запуске pp.exe может занять несколько секунд — PyInstaller распаковывает бандл во временную папку.

CLI

Кириллица и любые не-ASCII промпты безопасны: при не-UTF-8 локали CLI перезапускается в UTF-8 Mode, а битую вставку из терминала (привет) pp add детектирует и чинит автоматически.

pp add "промпт"                        # добавить задачу (дефолтный провайдер)
pp add "промпт" -c codex               # через Codex
pp add "промпт" -c qwen                # через Qwen
pp add "промпт" -c claude-z            # через кастомный алиас
pp add "промпт" -p 1                   # с приоритетом (1 = высший)
pp add "промпт" -a "2026-03-25T03:00"  # запланировать на время
pp add -f prompts.txt                  # добавить из файла (по строке)
pp add "промпт" -d /path/to/project    # задать рабочую директорию
pp add "промпт" -d /repo -w            # в своём git worktree (ветка pp/t<id>)

pp list                                # задачи (по умолчанию последние 20; -n N — больше)
pp list -s pending                     # фильтр по статусу
pp status 1                            # детали задачи #1
pp cancel 1                            # отменить задачу
pp delete 1                            # удалить задачу
pp stats                               # статистика
pp purge --days 7                      # удалить старые завершённые задачи

pp note 42 "перечитай комментарий"      # дописать решателю к задаче #42
pp note 42 --clear                     # убрать приписку

pp usage                               # расход за окно лимита (5 ч)
pp usage --hours 24 --json             # то же машинно, за сутки

pp guard --rules                       # правила сторожа запретов
pp guard "git push origin main"        # что сторож сделает с командой

pp worker                              # запустить воркер
pp server                              # запустить веб-UI
pp server -p 9000                      # на другом порту
pp bot                                 # запустить Telegram бот

Telegram бот

Настройка

  1. Создай бота через @BotFather, получи токен.
  2. Задай переменные окружения:
$env:PP_TG_TOKEN = "токен-от-botfather"
$env:PP_TG_ALLOWED_PHONES = "+79001234567,+79007654321"
  1. Запусти:
pp bot

Авторизация

При первом открытии бота пользователь видит кнопку «Поделиться контактом». Бот получает номер телефона и сверяет с PP_TG_ALLOWED_PHONES. При совпадении — доступ открыт.

Авторизованные пользователи сохраняются в ~/.promptpilot/tg_users.json. Повторная авторизация при перезапуске не нужна.

Альтернатива env-переменной — файл ~/.promptpilot/tg_config.json:

{
  "allowed_phones": ["+79001234567", "+79007654321"]
}

Возможности бота

Функция Описание
📋 Задачи Список задач с пагинацией и статусами
🖥 Окна (/windows) Живые herdr-сессии по всем машинам: статус, проект, хвост экрана; в карточке — 💬 промпт прямо в панель и клавиши permission-диалога
➕ Добавить задачу Промпт → провайдер → модель (если у провайдера есть список) → приоритет → skip-permissions → оставить herdr-сессию? (для herdr) → директория → расписание → повтор → режим запуска
📊 Статистика Сводка по статусам
🔌 Провайдеры Список провайдеров с деталями: команда, модели, env-переменные (ключи маскированы), источник настройки
Детали задачи Промпт, результат, ошибка; кнопки: отмена (работает и для running — процесс будет убит), сброс, удаление
💬 Ответить Продолжить диалог с моделью в той же сессии
⚡ Скилы (/skills) Список Claude Code скилов; выбор запускает пошаговое создание задачи
🔔 Уведомления Автоматически присылает результат или ошибку после завершения задачи
📎 Вложения Скриншот или файл прямо в мастере: подпись к фото становится текстом задачи

Вложения: скриншоты и файлы

Файл можно приложить и в боте, и в веб-интерфейсе — агент получит абсолютный путь к нему приписанным к промпту («Приложенные файлы (читай по этим путям)») и прочитает файл сам.

В боте — на шаге ввода промпта или на карточке подтверждения: пришли фото (подпись к нему станет текстом задачи) либо документ. Файлы принимаются и на остальных шагах мастера — альбом из нескольких скриншотов Telegram отправляет отдельными сообщениями, они долетают уже после того, как мастер шагнул дальше. Ограничение Telegram: боту не отдают файлы больше 20 МБ.

В вебе — кнопка 📎, перетаскивание в поле промпта или просто Ctrl+V скриншота из буфера.

Где лежат файлы: ~/.promptpilot/attachments/<uuid>/<имя> (бот) и ~/.promptpilot/uploads/ (веб). Наружу каталоги не раздаются. Файлы недосозданной задачи бот удаляет сам, а вложения созданных задач остаются на диске — автоочистки по возрасту пока нет.

Ограничение: вложения работают только для задач на локальной машине. Файл лежит на этом хосте, и агент на другой машине его не увидит — бот и веб в этом случае не дадут запустить задачу и скажут, почему.

Продолжение сессии (💬 Ответить)

После завершения задачи в деталях появляется кнопка 💬 Ответить — если модель спросила что-то или ты хочешь продолжить диалог:

  1. Открой детали завершённой задачи → нажми 💬 Ответить
  2. Введи ответ или следующий вопрос
  3. Бот создаст новую задачу с флагом --resume <session_id> — Claude продолжит разговор в том же контексте

Цепочка не ограничена: каждый «ответ» тоже получает кнопку 💬. Новая задача наследует провайдера, рабочую директорию и флаги оригинальной.

Фоновый запуск (detached)

При создании задачи через бота последний шаг — выбор режима запуска:

Как запустить?
[▶ Обычно (ждать результата)]  [🔁 Фоново (сервер/бот)]

Обычно — воркер ждёт завершения команды и сохраняет результат. Подходит для разовых задач.

Фоново — процесс запускается отдельно и сразу отвязывается. Задача помечается completed (PID XXXX), воркер переходит к следующей задаче. Процесс живёт независимо до ручной остановки.

Когда использовать «Фоново»:

  • Запустить другой бот / сервер
  • Скрипт с бесконечным polling-циклом
  • Любой процесс, который никогда не завершится самостоятельно

Примечание: в режиме «Фоново» воркер не перехватывает вывод и не знает об ошибках после старта. Если процесс упал сразу — в задаче это не отобразится.

Просмотр настроек провайдеров (🔌 Провайдеры)

Кнопка 🔌 Провайдеры показывает inline-кнопки со списком провайдеров. При нажатии — карточка с деталями:

  • Команда запуска (или «Исполнитель: herdr» для herdr-провайдеров)
  • Список моделей (или «по умолчанию»)
  • Env-переменные (API-ключи маскированы: sk-...xyz)
  • Источник настройки (builtin, providers.json, или оба) и путь к файлу

Защита паролем (PP_TASK_PASSWORD)

Если задана переменная PP_TASK_PASSWORD, бот запрашивает пароль перед созданием задачи. При неверном вводе создание отменяется; введённое сообщение автоматически удаляется из чата.

PP_TASK_PASSWORD=mysecretpassword

Просмотр задач и статистика паролем не защищены — только создание.

Провайдеры

Встроенные провайдеры:

Имя Описание Скилы Выбор модели
claude Claude Code (Anthropic) — дефолт ✅ sonnet / opus / haiku
claude-z Claude Code с альтернативным API (GLM, z.ai и др.) ✅ sonnet / opus / haiku
codex OpenAI Codex
qwen Qwen Code
cursor Cursor Agent
opencode OpenCode AI (GPT-4o/5, o1/o3 и др.) ✅ из списка моделей

Любой провайдер с supports_skills=True считается Claude Code-совместимым и получает выбор модели автоматически.

Команды управления:

pp provider                   # список всех (+пометки hidden / not installed)
pp provider add <name> ...    # добавить
pp provider hide <name>       # убрать из списков Web UI и бота (unhide — вернуть)
pp provider remove <name>     # удалить

Кастомные провайдеры сохраняются в ~/.promptpilot/providers.json. Провайдеры, чей исполняемый файл не найден на машине, автоматически не показываются в списках Web UI и бота; ненужные (например claude-z без ключа z.ai) можно скрыть вручную: pp provider hide claude-z.

Дефолтный провайдер: переменная PP_DEFAULT_CLI (по умолчанию claude).

Путь к claude ищется в PATH автоматически (claude на Linux/macOS, claude.exe на Windows), переопределяется через PP_CLAUDE_EXE. opencode ищется в PATH, затем в ~/.opencode/bin (официальный установщик) и npm-биндирах.

Добавление кастомного провайдера

pp provider add myai \
  --cmd "myai run {prompt}" \
  --desc "My AI Tool"

С переменными окружения и поддержкой скилов:

pp provider add claude-z `
  --cmd "C:\Users\<username>\.local\bin\claude.exe -p --verbose --output-format stream-json {prompt}" `
  --desc "Claude Code (GLM via z.ai)" `
  --env "ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic" `
  --env "ANTHROPIC_AUTH_TOKEN=your-token-here" `
  --env "ANTHROPIC_DEFAULT_SONNET_MODEL=glm-4.7" `
  --env "ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.7"

Windows: subprocess не видит PowerShell-функции и алиасы — нужен полный путь к исполняемому файлу. .cmd/.bat-обёртки (npm-инструменты вроде qwen, codex) находятся автоматически через shutil.which.

Интеграция с herdr (Linux/macOS)

herdr — терминальный мультиплексор для AI-агентов. PromptPilot умеет выполнять задачи не headless-процессом, а в живой herdr-сессии: агент виден, к нему можно подключиться и вмешаться, а главное — задача, упёршаяся в permission-диалог, не падает и не требует --dangerously-skip-permissions: агент переходит в состояние blocked, в Telegram приходит уведомление, вы подтверждаете действие — задача продолжается.

Провайдер с исполнителем herdr описывается так (~/.promptpilot/providers.json):

"claude-herdr": {
  "executor": "herdr",
  "kind": "claude",
  "description": "Claude Code в herdr-сессии",
  "supports_skills": true
}

Добавить можно тремя способами:

Web UI — кнопка ⚙ Providers → форма «Добавить провайдер» → тип «herdr-сессия» → указать имя, агента (kind) и, при желании, список моделей через запятую — тогда при создании задачи появится выпадашка выбора модели.

CLI:

pp provider add claude-herdr --executor herdr --kind claude --desc "Claude в herdr"
pp provider add opencode-herdr --executor herdr --kind opencode \
  --desc "OpenCode в herdr" \
  --models "opencode/big-pickle,opencode/mimo-v2.5-free,opencode/deepseek-v4-flash-free"

providers.json — вручную, поле "models": [...] опционально.

kind — любой агент, который поддерживает herdr: claude, codex, gemini, cursor, opencode, grok, copilot, droid, amp, kilo и другие (полный список: herdr agent start --help). Список доступных моделей opencode: opencode models. Выбранная модель передаётся агенту флагом --model при старте сессии.

Режимы завершения:

  • «🖥 оставить сессию открытой» — галочка при создании задачи (Web UI; в боте — отдельный шаг для herdr-провайдеров), по умолчанию включена: результат сохраняется в базу, но сессия остаётся — приходите, читаете транскрипт и продолжаете диалог в той же панели. Снятая галочка — панель закрывается после задачи. (Провайдерный флаг "keep_pane": true тоже поддерживается и форсирует режим.)
  • detached (галочка «Фоновый запуск» в боте) — промпт отправлен, задача сразу completed, агент работает в открытой панели.

Ограничения: интерактивная сессия не отдаёт stream-json, поэтому cost/session_id для herdr-задач не считаются (дашборд стоимости и кнопка 💬 неактивны).

Задачи в открытую сессию (herdr-session)

Встроенный провайдер herdr-session отправляет промпт в уже открытую панель herdr — оставленную задачей (pp-kept-N) или запущенную вручную:

  • Web UI: выберите провайдера herdr-session — появится выпадашка живых сессий (панель · агент · статус · имя); в боте — те же кнопки.
  • Работает вся механика задач: очередь, расписание («в 09:00 подведи итоги»), recurrence, blocked-уведомления, отмена, результат в базу.
  • Чужая панель никогда не закрывается и не переименовывается; если панель закрыли до запуска — задача падает с понятной ошибкой (и уведомлением).
  • Так удобно строить цепочки продолжений в одну сессию — аналог «💬 Ответить» для herdr-задач.

Экран «🖥 Окна» в боте (/windows)

Обратная сторона моста: не ждать уведомления, а самому зайти и посмотреть, чем заняты агенты. Список живых панелей собирается по всем машинам с herdr (agent list), в карточке — статус, проект, хвост экрана и действия:

  • 💬 Ответить — промпт уходит прямо в панель (agent prompt), мимо очереди задач: быстро, но без записи в задачи и без учёта стоимости.
  • Enter / 2 / 3 / Esc — ответ на permission-диалог, когда агент blocked.
  • Панели задач PromptPilot помечены номером задачи (#42).

Отправленные так промпты журналируются в таблицу prompt_log — время, проект (cwd панели), машина, панель и текст; сами транскрипты не дублируются, их хранит агент (~/.claude/projects/…). Выключается через PP_LOG_PROMPTS=0.

Экран агента в Web UI

У запущенной herdr-задачи раскрытая карточка показывает блок Экран агента: хвост панели (обновляется раз в 4 секунды, пока карточка раскрыта), статус агента и кнопки Enter / 2 / 3 / Esc — тот же ответ на permission-диалог, что в боте, но с десктопа. Раньше заблокированная задача выглядела в вебе как бесконечный «running now», и разблокировать её можно было только из Telegram или из самого herdr.

Эндпоинты сознательно узкие: GET /api/tasks/{id}/screen и POST /api/tasks/{id}/keys работают только через id задачи (не через произвольный pane_id), а список клавиш — ровно enter, 2, 3, esc. Произвольного ввода в чужой терминал через веб нет.

Как изменить провайдера

Web UI: ⚙ Providers → «Изменить» у нужного провайдера — форма заполнится текущими значениями; сохранение с тем же именем перезаписывает. У встроенных провайдеров так создаётся переопределение (сбрасывается кнопкой «Сбросить к встроенному»). Замаскированные env-секреты при редактировании нужно ввести заново.

Расписание: серии, временное ускорение и анализ очереди

Повторяющаяся задача хранится как постоянная серия, а каждый запуск — отдельное вхождение этой серии. Кнопка 🗓 Расписание в Web UI показывает период, параметры (провайдер, модель, эффорт, машина), следующий запуск, результат прошлого прогона и статистику здоровья. Кнопка «Изменить» правит всю серию: основной интервал, effort, приоритет и таймаут сохраняются для будущих запусков, даже если текущий уже работает.

Для временного разбора накопившейся очереди есть «ускорение»: например, основной интервал 4h, временный 1h. PromptPilot автоматически вернёт 4h по указанной дате или после N последовательных итогов ПУСТО. Серия также ставится на паузу, возобновляется, запускается немедленно или окончательно завершается из Web UI и Telegram (/schedule). При сокращении интервала уже созданный будущий запуск переносится ближе сразу; ждать старого четырёхчасового scheduled_at больше не нужно. Более длинный интервал, наоборот, не откладывает запуск, который уже должен состояться раньше.

В общей очереди карточка повторяющейся задачи показывает короткое имя серии, а полный служебный промпт остаётся внутри раскрытой карточки. Если серия поставлена на паузу, будущий запуск явно помечается PAUSED и не выглядит как готовый к исполнению queued.

Отдельная кнопка «📈 Конвейеры» в главной панели и команда бота /pipeline показывают любой настроенный pipeline-профиль. Аналитика считает внешний backlog через GitHub CLI, сопоставляет его с ёмкостью одного прогона и сохраняет локальные снимки по profile_id. Узкое место — этап с максимальным ETA: учитываются backlog / capacity, интервал серии и средняя длительность её выполнения. PromptPilot не поставляет профили конкретных проектов. Метки, запросы и лимиты задаются пользователем в ~/.promptpilot/pipeline_profiles.json (или файле из PP_PIPELINE_PROFILES), поэтому чужие репозитории не появляются в новой установке. Если в профиле включён priority_control, дашборд показывает элементы очередей и позволяет поставить P0…P3 либо вернуть автоматическую оценку. Кнопка «⚡ Следующим» ставит P0 и переносит будущий запуск соответствующей серии на ближайшее время; поставленная на паузу серия при этом остаётся на паузе. Одинаковые диагностические сигналы в окне конвейера сворачиваются в одну строку со списком и количеством PR; владелец single-flight барьера всегда показывается первым, чтобы было видно, какой PR сейчас должен пройти REVIEW → MERGE. В основной карточке служебные коды и сырая health-сводка заменяются понятными формулировками: какой PR следующий, выполняется ли REVIEW прямо сейчас, сколько решений ждут человека и сколько PR ещё переходят со старого формата протокола.

Снимки активных профилей собираются фоново каждые пять минут (PP_PIPELINE_SNAPSHOT_INTERVAL, 0 отключает). Дашборд показывает Δ backlog, вход/выход, переходы между очередями, churn, возраст элементов и смысловые итоги прогонов за 5 и 24 часа. Это обычные GitHub API, SQLite и арифметика — LLM не вызывается и токены не расходуются. После первого запуска историческим карточкам нужно накопить соответствующее окно; до этого интерфейс честно показывает покрытие. В таблице каждого этапа видны номер, время и итог последнего завершённого запуска; отдельная карточка показывает время последнего REVIEW. Для Codex PromptPilot использует JSONL-режим CLI и сохраняет точные input/output tokens. Старые текстовые запуски остаются с пометкой «нет данных»: их расход не оценивается по длительности и не выдумывается задним числом. «ГОТОВО без действий» учитывается отдельно от продуктивных прогонов.

Идентификатор JSONL-сессии сохраняется сразу после события thread.started, а не только в финале. Если worker или процесс Codex оборвался после создания ветки/worktree, retry запускает codex exec resume для той же сессии. Благодаря этому продолжение знает о собственных незавершённых изменениях и не объявляет их «чужой работой» после повторного запуска.

stop.ps1 не полагается только на .pp-pids.json: после hot-reload PID worker меняется. Скрипт получает актуальный worker PID из heartbeat API, server PID — из слушателя PP_PORT, проверяет командную строку процесса и лишь затем останавливает дерево. Поэтому штатный stop не оставляет скрытый worker, который продолжает расходовать токены после «остановки» старого launcher PID.

Рекомендация считается прозрачно: backlog / capacity даёт число необходимых прогонов, а один цикл равен интервал + средняя длительность прогона — повтор назначается после завершения предыдущего запуска. Поэтому серия 15m со средним выполнением 30 минут даёт примерно один результат за 45 минут, а не четыре в час. Профиль задаёт желаемое время очистки (target_clear_hours, обычно 8 часов), интервал выбирается из практичных пресетов. Если одна длительность прогонов уже не позволяет уложиться в цель, дашборд предлагает увеличить ёмкость или параллелизм. ETA — нижняя оценка: она не учитывает новые входящие задачи и ожидание более приоритетных серий в общей очереди worker; для этапа с manual_gate: true ускорение не предлагается, потому что его ограничивает решение человека.

Worker публикует heartbeat в общей SQLite БД. Активный профиль немедленно становится красным, если heartbeat отсутствует или устарел; running, который пережил настроенный task_timeout, отдельно показывается как зависший запуск. Это позволяет отличить медленную очередь от остановившегося исполнителя без ожидания пятичасового окна статистики.

Несколько проектов и цепочек

Каждая строка расписания — самостоятельная серия со своим working_dir. Аналитический профиль объединяет нужные серии в одну логическую цепочку по series_contains. Несколько проектов — это несколько профилей; раздел «Конвейеры» покажет их в выпадающем списке и не смешивает снимки. Пользовательский файл по умолчанию: ~/.promptpilot/pipeline_profiles.json:

{
  "profiles": {
    "my-project": {
      "title": "MyProject: сопровождение",
      "repository": "owner/my-project",
      "target_clear_hours": 8,
      "priority_control": {
        "trusted_account": "YOUR_GITHUB_LOGIN",
        "default_level": "p2",
        "aging_hours": 168,
        "max_items": 12
      },
      "queues": [
        {
          "id": "fix",
          "title": "Исправления",
          "query": "is:issue is:open label:ready-fix -label:in-work",
          "capacity": 1,
          "series_contains": "MyProject - FIX",
          "backlog_diagnostic_field": "fix_candidates",
          "wake_when": {"field": "fix_candidates"},
          "dispatch_gate": {
            "skip_when_empty": true
          }
        },
        {
          "id": "review",
          "title": "Ревью",
          "query": "is:pr is:open -label:ship -label:changes-requested -label:needs-decision -label:hold",
          "capacity": 2,
          "series_contains": "MyProject - REVIEW",
          "backlog_diagnostic_field": "review_backlog",
          "wake_when": {"field": "review_candidates"}
        },
        {
          "id": "merge",
          "title": "Слияние",
          "query": "is:pr is:open label:ship",
          "capacity": 1,
          "series_contains": "MyProject - MERGE",
          "backlog_diagnostic_field": "merge_candidates",
          "wake_when": {"field": "merge_candidates"},
          "dispatch_gate": {
            "skip_when_empty": true,
            "defer_when_diagnostics_match": [{
              "field": "review_candidates",
              "key": "stage",
              "values": ["integration-review", "legacy-integration-review"]
            }],
            "defer_for": "10m"
          }
        }
      ],
      "health_check": {
        "command": ["/absolute/path/to/project-health", "-json"],
        "working_dir": "/absolute/path/to/project",
        "timeout_seconds": 180
      }
    }
  }
}

priority_control необязателен. Он использует GitHub-метки queue:p0queue:p3 для ручного решения и queue:auto:p0queue:auto:p3 для оценки, перенесённой с issue на PR. Ручная метка старше автоматической; без обеих critical/security получает P0, bug — P1, enhancement/documentation/default — P2, question — P3. Каждые aging_hours ожидания эффективный уровень повышается на один до P1, поэтому низкий приоритет не означает вечное ожидание, а P0 остаётся полосой для действительно срочной работы. Если проектный исполнитель сортирует кандидатов сам, он должен применять те же правила: PromptPilot управляет метками и показывает порядок, но не подменяет безопасный выбор внутри репозитория. Проверка trusted_account не даёт случайно менять приоритет под другой учётной записью GitHub CLI.

health_check необязателен. Это проектная команда без shell, которая должна вернуть JSON со state: green|yellow|red, summary и массивом findings. PromptPilot запускает её при каждом снимке и показывает результат сразу, поэтому нарушение текущих инвариантов видно без ожидания 5- или 24-часовой истории. Команда не должна вызывать LLM; для GitHub-проверок путь из PP_GH_EXE передаётся ей как GH_EXE. Таймаут по умолчанию — 180 секунд; для больших репозиториев его можно увеличить до 900. Ошибка или timeout самой команды отображается как «диагностика не выполнена», а не как доказанное нарушение инварианта.

dispatch_gate тоже необязателен и настраивается у конкретного этапа. Перед запуском провайдера PromptPilot получает свежий снимок профиля. При skip_when_empty пустой запуск завершается как ПУСТО, не запуская LLM. Поле defer_when_diagnostics_nonempty задаёт простую зависимость от непустого массива в JSON health_check. Когда один массив содержит несколько состояний, используйте defer_when_diagnostics_match: в примере MERGE возвращается в pending только для элементов со stage integration-review/legacy-integration-review, но проходит для integration-merge-ready. Это даёт автоматический порядок REVIEW → MERGE даже при более высоком приоритете MERGE и не расходует токены на ожидание. Если профиль или checker сломан, gate fail-open: задача запускается обычным способом, чтобы ошибка наблюдаемости не остановила полезную работу.

wake_when необязателен. Он указывает поле диагностики, которое означает, что этапу уже есть что делать. После любого полезного (ГОТОВО) запуска и при фоновом обновлении профиля PromptPilot переводит ближайший pending-запуск этой серии на «сейчас». Поэтому FIX → REVIEW и REVIEW → MERGE не ждут следующего периодического интервала. Можно добавить key и values, если готовность определяется только некоторыми состояниями элементов. При ПУСТО, паузе серии или пустом поле пробуждения нет.

backlog_diagnostic_field заменяет приблизительный размер GitHub Search точным массивом из health_check. Это особенно важно для REVIEW: поисковый запрос намеренно не исключает stale-метку reviewed, а checker уже различает новый HEAD, актуально проверенный HEAD и интеграционный handoff. Для REVIEW используйте review_backlog (вся ожидающая работа), а wake_when оставьте на review_candidates (только исполняемая сейчас работа).

Не исключайте reviewed из поискового запроса REVIEW: метка относится к старому HEAD и после нового push может остаться. Канонический health_check отличает актуальное ревью (reviewed_waiting_ship) от устаревшей метки и возвращает новый HEAD в review_candidates. Для двухполосной схемы он также публикует content_review_candidates, integration_owner и merge_candidates: single-flight сериализует интеграцию, но не останавливает содержательные ревью.

Детерминированный pipelinectl и fallback на скилл

Для REVIEW/MERGE повторяемые GitHub-проверки можно вынести из промпта в проектный CLI. PromptPilot не привязан ни к Claude, ни к Codex: он сам выполняет безопасный preflight до запуска провайдера и только затем решает, нужна ли модель. В очередь профиля добавляется:

"execution": {
  "mode": "auto",
  "stage": "review",
  "command": [
    "{python}", "-m", "promptpilot.project_pipeline",
    "--config", "pipelinectl.json", "next", "{stage}"
  ],
  "required_paths": ["pipelinectl.json"],
  "probe_command": [
    "{python}", "-m", "promptpilot.project_pipeline",
    "--config", "pipelinectl.json", "capabilities"
  ]
}

{python} заменяется интерпретатором запущенного PromptPilot, {stage}id этапа. Команда и относительные пути проверяются в working_dir серии без вызова LLM, после чего command выполняется с таймаутом timeout_seconds (по умолчанию 180 секунд). Режимы:

Режим Поведение
auto empty/wait завершается без модели; audit/merge получает короткий prompt с готовым lease; fallback или недоступный CLI использует исходный prompt/скилл
tool CLI обязателен; при отсутствии, ошибке или fallback запуск завершается НУЖЕН ЧЕЛОВЕК без токенов
skill всегда используется исходный prompt, как до появления pipelinectl

Эффективный маршрут (auto → tool, auto → skill или blocked) виден в дашборде и боте для каждого этапа. Это позволяет заметить незапланированный fallback сразу, а не по суточному расходу токенов. Пустой REVIEW/MERGE закрывается самим preflight с явной записью «провайдер не запускался».

Встроенный promptpilot.project_pipeline реализует общий protocol v1. Проект задаёт repository, доверенный аккаунт, health-команду, base branch и required checks в pipelinectl.json. Обычный REVIEW получает ровно один кандидат и opaque lease; complete review повторно проверяет HEAD и два одинаковых полных GraphQL snapshot, затем сам выполняет review → claim → label → completion. Обычный CLEAN MERGE аналогично повторяет proof/labels/CI и использует merge с точным SHA. Base-sync, carry, legacy re-ship, конфликт, recovery и третий круг намеренно возвращают action=fallback: их продолжает полная проектная процедура. Таким образом быстрый путь не ослабляет сложные гейты.

Claude и Codex получают одну и ту же команду и JSON. Различаются только тонкие файлы обнаружения навыка (.claude/skills и .agents/skills); логика CLI и lease от модели не зависят.

Статус в Web UI и боте читается так:

Статус Значение
green — «работает штатно» Инварианты соблюдены, активных ожиданий нет
yellow — «есть ожидания» Выполняется восстанавливаемая транзакция или нужен ход человека; это не поломка
red — ошибка Worker не работает, запуск превысил timeout, серия оборвана либо нарушен проектный инвариант

Причина и конкретные findings выводятся рядом со статусом. Поэтому жёлтый индикатор сам по себе не требует останавливать очередь: сначала нужно прочитать, какое именно ожидание он показывает.

Дашборд внешнего конвейера в PromptPilot

Жёлтый статус означает ожидаемый handoff или ход человека; подробная причина видна в той же карточке.

У двух цепочек могут быть одинаковые этапы: добавьте проект в первую строку промпта (MyProject - REVIEW, OtherProject - REVIEW) и используйте такое же уникальное значение в series_contains. Профиль влияет только на аналитику; интервалы, effort, пауза и временное ускорение принадлежат самим сериям.

Сейчас профиль и серии связываются по названию. Отдельного мастера «Создать всю цепочку из шаблона» пока нет: этапы создаются как повторяющиеся задачи, а потом собираются профилем. Это важное отличие от Workflow-оркестратора: workflow ведёт конечную разработку «исполнитель → аудитор → исправление», а расписание обслуживает бесконечные дежурные очереди TRIAGE/FIX/REVIEW/MERGE.

Серия без запланированного вхождения помечается «оборвана» — она сама не продолжится. Раньше в это состояние приводило любое падение: следующее вхождение создавалось только после успешного прогона, поэтому один rate limit тихо убивал дежурную задачу насовсем. Теперь расписание продлевается и после failed, а в Telegram уходит «🔁 задача упала, но расписание продолжено — следующий запуск в …». Отменённая серия не продлевается: отмена — это решение человека, а не сбой.

Одно вхождение в очереди (pending/rate_limited) по-прежнему можно править в карточке. Для постоянного изменения расписания используйте экран серии.

Эффорт (усилие рассуждений)

  • Claude Code и Codex — уровень low|medium|high|xhigh|max задаётся полем и работает на двух уровнях:

    • у провайдера — «Эффорт рассуждений» в форме провайдера (⚙ Providers) или pp provider add ... --effort max. Это дефолт: с ним живут все задачи провайдера, в том числе этапы конвейеров;
    • у задачи — селект «Effort» рядом с «Model» в веб-мастере и кнопка «Эффорт» в разделе «Дополнительно» бота. Перекрывает провайдера на один прогон и наследуется следующими вхождениями повторяющейся задачи.

    Пустое значение = «по умолчанию»: PromptPilot не передаёт override и уровень выбирает сам CLI. Для Claude используется --effort, для Codex — одноразовый -c model_reasoning_effort=...; глобальная конфигурация агента не меняется. Если настройка уже вписана руками в шаблон команды или аргументы провайдера, выигрывает написанное руками. Остальным агентам effort не передаётся.

  • OpenCode — два пути:

    • headless: флаг --variant high|max|minimal в cmd-шаблоне (opencode run --variant max {prompt});

    • herdr-сессия: у TUI флага запуска нет, но variant задаётся через агент-профиль в ~/.config/opencode/opencode.jsonc:

      { "agent": { "max": { "mode": "primary", "variant": "max" } } }

      и доп. аргументы провайдера --agent max.

Заводить отдельных провайдеров под каждый уровень (claude-herdr и claude-herdr-max) больше не нужно — но если такие уже есть, они продолжают работать: эффорт провайдера просто становится их дефолтом.

Побочно: cmd-провайдер, собранный вокруг самого claude (клон встроенного через «Изменить»), теперь отмечается как Claude-провайдер автоматически — ему достаются скилы, выбор модели, --resume и эффорт. Раньше такой клон выглядел «неизвестным CLI» и молча ронял всё перечисленное.

herdr-плагин

Минимальный плагин для herdr 0.7.5+ лежит в herdr-plugin/. Установка:

herdr plugin install ivanarama/PromptPilot/herdr-plugin   # из GitHub
herdr plugin link /path/to/PromptPilot/herdr-plugin       # локальная копия

Плагин — тонкая обёртка над pp: сам PromptPilot он не ставит и без него не работает. Если pp на машине нет (не в PATH, не в ~/.local/bin, модуль promptpilot не импортируется), плагин не пытается запустить что-то наугад — он показывает уведомление herdr со ссылкой на установку и пишет то же самое в ~/.promptpilot/startup.log.

Что даёт:

  • автозапуск worker'а — при старте herdr-сервера плагин поднимает pp worker, если тот ещё не запущен (уже работающий не трогается);
  • постановка задачи из панели — action «PromptPilot: поставить задачу» (палитра действий, herdr plugin action invoke enqueue --plugin promptpilot или клавиша ctrl+alt+e) открывает popup: ввёл текст — задача ушла в очередь с рабочей директорией текущей панели.

После правок манифеста нужен herdr plugin unlink promptpilot + повторный link.

Свой git worktree на задачу

По умолчанию агент работает прямо в указанной директории — то есть в том же рабочем дереве, где сидишь ты. Галка 🌿 свой worktree (Web UI, шаг мастера в боте, pp add -w, поле worktree: true в API) меняет это: задача получает собственный чекаут репозитория на ветке pp/t<id>.

Что это даёт:

  • твоё рабочее дерево остаётся нетронутым — незакоммиченные правки в безопасности, ветка не переключается;
  • результат — ветка, а не грязный git status: смотришь diff, вливаешь или выбрасываешь целиком;
  • retry после rate-limit возвращается на ту же ветку и в тот же чекаут, а не наслаивается на полуизменённое дерево;
  • две задачи по одному репозиторию могут идти параллельно (см. PP_CONCURRENCY).

Куда попадает чекаут:

Исполнитель Расположение
herdr-провайдеры herdr создаёт worktree-workspace сам (herdr worktree create) — его видно в UI herdr, там же можно закрыть или удалить. Работает и на удалённой машине
обычные (headless) <родитель репозитория>/.pp-worktrees/<repo>-t<id>, либо PP_WORKTREES_ROOT/<repo>/t<id>

Путь и ветка сохраняются в задаче и показываются в Web UI, боте и pp status <id> — вместе с короткой сводкой «коммитов: N, незакоммиченных файлов: M».

Нюансы:

  • Директория обязана быть git-репозиторием. Не репозиторий — галка в Web UI недоступна, шага в боте нет, а задача, созданная через API, честно падает с ошибкой вместо тихого запуска в общем дереве.
  • Игнорируемые файлы не переезжают. Свежий чекаут — без .env, node_modules, venv. PromptPilot копирует в него то, что перечислено в PP_WORKTREE_COPY (по умолчанию .env) — и только те файлы, которые git действительно игнорирует. Остальное — сборка, установка зависимостей — на совести самой задачи.
  • Чекаут не удаляется после задачи — в нём результат. Исключение: если агент не оставил ни коммитов, ни изменений, herdr-исполнитель убирает пустой чекаут за собой. Ветка pp/t<id> остаётся всегда.
  • На удалённых машинах worktree поддерживают только herdr-провайдеры: headless-команда по ssh выполняется в домашней директории и в чекаут просто не зайдёт — такая задача падает с явным сообщением.

Параллельные задачи (PP_CONCURRENCY)

PP_CONCURRENCY=1 (по умолчанию) — worker берёт задачи строго по одной, как раньше. Больше единицы — задачи идут в пуле потоков, и очередь при этом обходит всё, что столкнулось бы с уже работающей задачей:

  • две задачи с одной рабочей директорией (на одной машине) одновременно не запустятся — второй агент подождёт, пока первый закончит;
  • задачи со своим worktree не блокируют никого и ничего: у каждой свой чекаут — именно в этом сочетании параллелизм и раскрывается;
  • задачи в одну и ту же открытую herdr-сессию (herdr-session) сериализуются по сессии.

Захват задачи атомарный (BEGIN IMMEDIATE + условный UPDATE), так что гонки за одну задачу нет. Предположение остаётся прежним: worker'ов — один процесс; второй при старте вернёт running-задачи первого в очередь (recover_running). Нужно больше параллелизма — поднимай PP_CONCURRENCY, а не второй worker.

Срыв по вине среды — не провал задачи

Раньше любой ненулевой код возврата означал failed. Но отказ в доступе (API Error: 401/403/5xx, Failed to authenticate) и обрыв ответа на полуслове (Connection closed mid-response, terminal_reason=api_error, ECONNRESET, EAI_AGAIN) — это не вина задачи. Причём обрыв почти всегда приходится на конец прогона: работа сделана и закоммичена, не доехало только последнее слово.

Теперь такая задача возвращается в очередь со статусом rate_limited и обычным экспоненциальным backoff'ом, а в error пишется, что именно случилось. Считается это против max_retries, так что навсегда сломанная среда всё-таки доводит задачу до failed, а не крутит её вечно.

Что НЕ считается срывом среды: API Error: 400, падения тестов, отсутствующие модули, ошибки компиляции — всё это провал задачи и честный failed. Коды сокетов (ENOTFOUND и прочие) сверяются с учётом регистра: иначе ModuleNotFoundError читался бы как обрыв связи.

Не начинать задачу, когда машине нечем дышать

PP_MIN_FREE_MB (по умолчанию 0 — проверки нет): если свободной памяти меньше, worker не берёт новую задачу и говорит об этом один раз, а не каждый опрос. Слоты PP_CONCURRENCY сами по себе ничего не знают о том, потянет ли машина ещё один прогон — а на деле она уходит в своп, и следом API начинает отказывать. На Linux читается MemAvailable, на Windows — GlobalMemoryStatusEx; там, где померить нельзя, очередь не останавливается.

Дописать решателю (приписка)

Прогон идёт, а видно, что копает не туда — не перечитал свежий комментарий, работает по старому описанию. Приписка к задаче добавляет пару фраз, которые пойдут в следующий прогон отдельным блоком после промпта, с прямым указанием, что это написано последним и главнее всего выше.

pp note 42 "перечитай комментарий, идёшь не туда"
pp note 42            # показать
pp note 42 --clear    # убрать

В Web UI — поле «Дописать решателю» в развёрнутой карточке задачи; в списке у такой задачи стоит пометка ✎ приписка.

Живёт при задаче, а не при прогоне — и это главное:

  • идущий прогон её уже не увидит — она уйдёт в следующий прогон (после rate limit или срыва среды); если нужно применить прямо сейчас, проще пересоздать задачу с уже вписанной припиской;
  • одноразовая: задача дошла до вердикта — приписка снимается, чтобы не лезть во все следующие прогоны;
  • но срыв по вине среды или rate limit её сохраняет — та попытка приписку так и не увидела;
  • в повторяющиеся копии задачи она не попадает: следующая копия создаётся из сохранённого промпта, а не из того, что получил конкретный прогон.

Итог задачи (ИТОГ)

PP_VERDICT=1 дописывает к промпту просьбу закончить одной строкой:

ИТОГ: ГОТОВО | УЖЕ СДЕЛАНО | НУЖЕН ЧЕЛОВЕК | НЕ СМОГ | ПУСТО

Итог парсится и хранится у задачи, показывается в Web UI, боте и pp status. Разница практическая: код возврата говорит только «процесс завершился», а 🟡 НУЖЕН ЧЕЛОВЕК в уведомлении сразу говорит, надо ли идти смотреть.

ПУСТО — тихий итог для повторяющихся задач: «проснулся по расписанию, делать нечего». Такая задача завершается как обычно (итог в базе, виден в списках), но уведомление в Telegram не отправляется — иначе дежурный робот, просыпающийся каждые два часа, превращает бота в будильник. Ошибки (failed) шлются всегда, независимо от итога.

Парсится он всегда, даже когда мы не просили — если агент сам закончил такой строкой, итог подхватится. Побеждает последнее совпадение: формат могли процитировать по дороге, а вердикт — это закрывающая строка.

Живые прогоны переживают перезапуск worker'а

Каждый прогон помечен в своём окружении переменной PP_TASK_ID, и метку наследует процесс агента. Поэтому живой прогон опознаётся по процессу, а не по нашим же записям — и переживает смерть worker'а.

При старте recover_running() возвращает в очередь зависшие running-задачи, но теперь обходит те, чей агент всё ещё работает: раньше перезапуск worker'а выдёргивал очередь из-под живых агентов, а второй процесс worker'а на старте отбирал задачи у первого. Поиск идёт по /proc (Linux); там, где так нельзя, поведение прежнее.

Расход: сколько сожгли за окно лимита

Дашборд стоимости считает деньги по результатам задач — а herdr-задачи стоимости не дают вообще: интерактивная сессия не отдаёт stream-json. Чем больше работы уходит в herdr, тем слепее был дашборд.

pp usage (и строка «Окно 5ч» в Web UI) закрывает дыру, разбирая транскрипты, которые Claude Code пишет в ~/.claude/projects/**/*.jsonl — в том числе для herdr-панелей:

За последние 5 ч — оценка по прайсу API:
  всего:   $18.40   26.1 млн токенов   сессий: 4
  задачи:  $12.05
  прочее:  $6.35   (живая переписка ест то же окно лимита)

Почему именно так:

  • Считаем по сообщениям, а не по кускам. В транскрипте каждое сообщение записано целиком и с полным usage — суммировать можно. В потоковом журнале headless-прогона наоборот: сообщение разбито на куски с частичным usage, и суммирование занижает выход в сотню раз. Поэтому для headless-задач по-прежнему берётся готовый итог из события result.
  • Лимит общий на человека, поэтому в счёт идут и твои собственные сессии — иначе на вопрос «сколько осталось» ответить нельзя.
  • Задача узнаётся по session_id, а если его нет — по своему worktree. Обычный working_dir в признаки не годится: его задача делит с человеком, и живая переписка засчитывалась бы задаче (проверено — так и было).
  • Кэш считается отдельно: чтение ×0.1 от входа, запись ×1.25. Без этого счёт мимо на порядок — кэш-чтений на порядок больше обычного входа.
  • Деньги — оценка по прайсу API, а не счёт. На подписке они не списываются; полезны как мера того, куда уходит окно.

Окно скользящее — «последние 5 часов», а не доля выбранной нормы: точку сброса API называет только событием лимита, которого в транскриптах нет.

Сторож запретов (guard)

Задача с --dangerously-skip-permissions не спрашивает человека ни о чём — значит, и остановить её диалогом нельзя. Останавливает PreToolUse-хук: он видит команду до запуска и срабатывает независимо от режима разрешений. Заблокированное — код 2 и причина в stderr, её читает модель; всё заблокированное пишется в ~/.promptpilot/guard.log.

По умолчанию (PP_GUARD=auto) сторож включается ровно там, где больше некому спросить: у задач с skip_permissions, и только для Claude Code провайдеров — формат хука их. PP_GUARD=1 — всегда, PP_GUARD=0 — никогда.

Что запрещено «из коробки»: удаление корня и домашнего каталога, удаление .git, форс-пуш и удаление веток на сервере, пуш в main/master (в том числе git push origin HEAD из main — ветка проверяется отдельно, регулярка не видит, что именно пушат), git worktree remove|prune (в чужих worktree идёт работа других задач), sudo, запись на устройства, выключение машины.

Правила намеренно узкие: срабатывать они должны на катастрофе, а не на слове sudo внутри сообщения коммита — поэтому команды опознаются по позиции в строке, а main-fix не считается веткой main. Посмотреть и проверить:

pp guard --rules                      # какие правила действуют
pp guard "git push origin main"       # что будет с этой командой (ничего не запускается)

Дополнить или заменить — ~/.promptpilot/guard.json:

{"extend": [{"pattern": "npm\\s+publish", "reason": "Публикация — только руками"}]}

{"replace": [...]} вместо extend отключает встроенные правила целиком. Сломанный или нечитаемый файл оставляет встроенные правила в силе — сторож, который сам себя разоружает из-за лишней запятой, хуже отсутствующего.

Ограничение: на удалённых машинах сторож не ставится — файл настроек с хуком лежит здесь, а путь к нему там ничего не значит.

Раздача задач на другие машины (SSH)

Машина — отдельное измерение задачи: регистрируете машины, а те же самые провайдеры работают на любой из них.

Web UI: ⚙ Providers → «🖥 Машины» → имя + user@host → «Добавить и проверить» — PromptPilot сам определяет по ssh, какие CLI есть на машине (login-shell PATH). После этого в форме задачи появляется поле Машина (💻 локально / vm2 / ...), и список провайдеров фильтруется по возможностям выбранной машины. В боте — тот же шаг «Где выполнить задачу?».

Как выполняется: ssh host bash -lc '<команда провайдера>' — исполняемый файл ищется в PATH удалённой машины, env-блок провайдера (например, claude-z с GLM) пробрасывается, stream-json проходит насквозь — стоимость, session_id и rate-limit-детекция работают как локально.

Требования: ssh по ключу (BatchMode), нужные CLI на машинах (и их PATH в ~/.profile). Ограничение: detached-задачи (фоновый запуск headless-процесса) — только локально; headless-команда выполняется в домашней директории удалённой машины.

Низкоуровневая альтернатива — обёртка scripts/pp-ssh-run <host> <cmd...> {prompt} как cmd-шаблон провайдера (полный контроль над командой и путями).

herdr на любой машине

herdr-провайдеры (включая herdr-session) работают на машинах так же, как локально: PromptPilot вызывает herdr CLI удалённой машины по ssh, а панель с агентом живёт там — подключиться к ней можно командой herdr --remote <host> (она есть в уведомлениях и в мета-строке результата).

  • Проба машины сама находит herdr-провайдеров: нужен herdr в login-shell PATH машины и CLI соответствующего kind (claude, opencode, ...). Если агент установлен не в системный PATH (например, opencode в ~/.opencode/bin), допишите путь в ~/.profile машины и нажмите «Перепроверить» — иначе провайдер в списке машины не появится.
  • herdr server на машине поднимается автоматически (herdr server в фоне), если он там ещё не запущен.
  • Работают все режимы: blocked-ожидание с уведомлением и кнопками, keep-pane, detached, herdr-session (выпадашка живых сессий показывает сессии выбранной машины).
  • Рабочая директория задачи должна существовать на целевой машине. Если её там нет (или поле пустое), herdr молча откроет панель в домашней директории машины — задача выполнится не там, где вы ждали.
  • Уже зарегистрированные машины нужно один раз «Перепроверить» — старая запись в machines.json не знает про herdr-провайдеров.

Пошагово: herdr на двух машинах

Дальше A — главная машина: на ней PromptPilot (worker, Web UI, бот) и общая очередь; B — вторая машина, на которую уезжают задачи. На B не нужны ни Python, ни PromptPilot — только herdr и сам агент.

1. На B поставить herdr и агента. Тем же способом, что и на A; обновление — herdr update. Версии herdr на A и B лучше держать одинаковыми: подключение herdr --remote требует совместимого протокола.

2. Проверить PATH login-шелла на B. PromptPilot ходит на машину как ssh B bash -lc '...', то есть видит PATH из ~/.profile/~/.bash_profile, а не из ~/.bashrc (он читается только интерактивными шеллами). С машины A:

ssh B "bash -lc 'command -v herdr; command -v claude'"

Обе строки должны напечататься. Если пусто — на B в ~/.profile:

export PATH="$HOME/.local/bin:$PATH"      # herdr, claude
export PATH="$HOME/.opencode/bin:$PATH"   # если нужен opencode

3. Настроить ssh по ключу (с A на B).

ssh-keygen -t ed25519          # если ключа ещё нет
ssh-copy-id user@B
ssh -o BatchMode=yes user@B true && echo OK

BatchMode обязателен: PromptPilot никогда не отвечает на запрос пароля. Если ключ с парольной фразой — держите ssh-agent и следите, чтобы воркер видел SSH_AUTH_SOCK.

4. Зарегистрировать машину. Web UI: ⚙ Providers → 🖥 Машины → имя vm2

  • user@B → «Добавить и проверить». Или через API:
curl -s -X POST localhost:8420/api/machines \
  -H 'Content-Type: application/json' -d '{"name":"vm2","host":"user@B"}'

В ответе — список найденных провайдеров; там должны быть claude-herdr и herdr-session. Если их нет — вернитесь к шагу 2 (herdr или агент не видны login-шеллу) и нажмите «Перепроверить» (POST /api/machines/vm2/probe).

5. Поставить задачу. В форме задачи: Машинаvm2, провайдер — claude-herdr, рабочая директория — путь, который существует на B. В боте это шаг «Где выполнить задачу?». Дальше PromptPilot сам поднимет herdr server на B (если не запущен), создаст вкладку pp-t<id>-*, стартует агента и отправит промпт; упёрся в permission-диалог — задача останется running, а в Telegram придёт уведомление с кнопками.

6. Подключиться к живой сессии на B. С машины A — herdr --remote user@B, на самой B — просто herdr. Эта же команда приходит в уведомлении и в мета-строке результата. Оставленные задачами сессии называются pp-kept-<id>; отправить в такую сессию следующий промпт можно провайдером herdr-session (выпадашка показывает сессии выбранной машины).

Если вторая машина на Windows

herdr есть и под Windows — нативный herdr.exe из preview-канала:

powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"

WSL не нужен. PromptPilot определяет, на каком языке разговаривать с машиной, сам — при добавлении/перепроверке машины и хранит это в machines.json полем "shell": "posix" | "powershell" (в списке машин видно значком 🐧/🪟):

  • posix — команда уходит как ssh host bash -lc '<...>';
  • powershell — как ssh host powershell -EncodedCommand <base64/UTF-16LE>. Кодирование не украшательство: виндовый sshd сначала отдаёт командную строку cmd.exe, и никакие кавычки через него живыми не проходят. В base64 нет символов, которые cmd трактует по-своему, поэтому скрипт доезжает байт в байт. Перед скриптом выставляется [Console]::OutputEncoding = UTF8 — иначе JSON herdr'а и любая кириллица возвращаются кракозябрами.

Что нужно на Windows-машине:

  1. herdr.exe и агент (claude) — в PATH пользователя, под которым ходит ssh (проверка: ssh winbox powershell -NoProfile -c "Get-Command herdr, claude").
  2. OpenSSH Server (Параметры → Приложения → Дополнительные компоненты) с входом по ключу; публичный ключ для админской учётки кладётся не в ~/.ssh/authorized_keys, а в C:\ProgramData\ssh\administrators_authorized_keys.
  3. Шелл по умолчанию менять не надо — PowerShell вызывается явно; работает и дефолтный cmd.exe.
  4. Порт, отличный от 22, задавайте алиасом в ~/.ssh/config на машине A — в поле «хост» можно писать имя алиаса.

Дальше всё как обычно: «Добавить и проверить» → в списке claude-herdr, herdr-session → задача с этой машиной → подключение herdr --remote winbox. Рабочая директория задачи — в виндовой записи (C:\work\proj).

Если на Windows-машине настроен Git Bash как shell для sshd, проба определит её как posix — это не ошибка: такой машиной можно рулить обычным bash -lc.

Если что-то не так

Симптом Причина / что сделать
В списке машины нет herdr-провайдеров ssh B "bash -lc 'command -v herdr'" пуст → PATH в ~/.profile (шаг 2), затем «Перепроверить»
«herdr server на машине … не запущен и не удалось его поднять» зайдите на B и запустите herdr server руками; проверьте ssh B "bash -lc 'herdr status server --json'"
ssh спрашивает пароль ключи не разложены или agent недоступен (шаг 3); на Windows-машине для админской учётки ключ кладётся в administrators_authorized_keys
Windows-машина определилась как posix и падает на ней sshd отдаёт bash (Git Bash/WSL), но herdr стоит нативный — либо уберите bash из DefaultShell и перепроверьте машину, либо держите herdr там же, где bash
Задача отработала «не в том каталоге» указанной рабочей директории нет на B — herdr молча ушёл в $HOME
Задача висит в running агент в blocked ждёт человека: подключитесь (herdr --remote B) или подтвердите кнопкой в Telegram

herdr → Telegram мост

Бот (если запущен) наблюдает за всеми агентами herdr — не только за задачами PromptPilot — на этой машине и на каждой зарегистрированной машине с herdr-провайдерами — и присылает уведомление, когда агент заблокирован диалогом или закончил работу незамеченным. В тексте уведомления видно, на какой машине агент. Кнопки под уведомлением: ✅ Подтвердить (Enter) · 📺 Экран · ✍️ Ответить — работают и для удалённых панелей, так что разблокировать агента на любой машине можно прямо с телефона. Отключение: PP_HERDR_WATCH=0.

Настройка Cursor Agent

npm install -g @nothumanwork/cursor-agents-sdk
winget install BurntSushi.ripgrep.MSVC

Добавь в .env:

CURSOR_API_KEY=crsr_your_key_here

Ключ: cursor.com/settingsAPI Keys. Первый запуск занимает ~60 секунд.

Скилы Claude Code

Скилы — команды (/skill-name) из ~/.claude/commands/, ~/.claude/skills/ и плагинов Claude Code. Доступны для всех провайдеров с supports_skills=True.

Web UI

При выборе Claude-провайдера под полем промпта появляется кнопка ⚡ Skills. Нажми — откроется список скилов с описаниями. Выбор подставляет /skill-name в промпт.

Telegram бот

Кнопка ⚡ Скилы в главном меню или команда /skills. Поддерживает глобальные скилы и скилы конкретного проекта (📁 Скилы проекта...).

REST API

GET /api/skills                                — все доступные скилы
GET /api/skills?provider=claude                — только если провайдер поддерживает скилы
GET /api/skills?provider=claude&workdir=/path  — + локальные скилы проекта

Веб-интерфейс

Минималистичный dark-theme UI на http://127.0.0.1:8420:

  • Выбор провайдера и модели (дропдаун модели появляется автоматически для Claude Code провайдеров)
  • Добавление задач с приоритетом и расписанием (Ctrl+Enter для отправки)
  • Чекбокс --dangerously-skip-permissions
  • ⚡ Skills — раскрывает список доступных скилов
  • Фильтры по статусу, раскрытие деталей задачи
  • Отмена (в т.ч. running-задач — процесс будет убит) и удаление задач
  • Recur, per-task таймаут, галочка «🖥 оставить сессию открытой» (herdr)
  • Кнопка ⏸ Pause воркера и панель стоимости по провайдерам
  • ⚙ Providers — управление провайдерами: список с пометками (встроенный/кастомный, «не установлен», «скрыт»), добавление (команда-шаблон или herdr-сессия: агент, доп. аргументы, модели, env), «Изменить», «Скрыть/Показать», «Удалить» / «Сбросить к встроенному»
  • В выпадашке провайдеров у формы задачи — только установленные и не скрытые
  • Автообновление каждые 5 секунд

Доступ с другой машины — через SSH-туннель:

ssh -L 8420:127.0.0.1:8420 user@server   # затем открой http://localhost:8420

Авторизация (опционально)

По умолчанию API открыт только на 127.0.0.1 и без авторизации. Если сервер нужно открыть наружу (PP_HOST=0.0.0.0), задайте токен:

PP_API_TOKEN=длинный-случайный-токен

Браузер покажет стандартное окно логина (имя любое, пароль — токен); скрипты ходят с заголовком Authorization: Bearer <токен> или curl -u x:<токен>. Без токена запросы получают 401.

REST API

GET    /api/tasks                  — список задач (?status=pending&limit=50)
POST   /api/tasks                  — создать задачу (все поля TaskCreate, вкл. keep_pane, worktree)
GET    /api/tasks/{id}             — детали задачи
PATCH  /api/tasks/{id}             — отменить (для running — worker убьёт процесс) / сменить приоритет
DELETE /api/tasks/{id}             — удалить
POST   /api/tasks/{id}/reset       — сбросить зависшую задачу в pending
GET    /api/stats                  — статистика по статусам
GET    /api/stats/costs            — стоимость: today / week / total / by_provider
GET    /api/stats/usage            — расход за окно лимита по всем сессиям (?hours=5)
POST   /api/tasks/{id}/note        — дописать решателю ({"text": "..."}; пустой текст убирает)
GET    /api/worker/status          — paused + state/heartbeat_at/age_seconds/pid
POST   /api/worker/pause|resume    — пауза/возобновление воркера
GET    /api/version                — проверка обновлений (кэш 24 ч)
GET    /api/providers              — провайдеры (description, supports_skills, models, available, hidden, executor)
GET    /api/providers/manage       — полная информация для настроек (env-секреты маскированы)
POST   /api/providers              — создать/изменить провайдера
DELETE /api/providers/{name}       — удалить кастомного провайдера
POST   /api/providers/{name}/hide|unhide — скрыть/показать в списках
GET    /api/skills                 — скилы (?provider=claude&workdir=/path)
GET    /api/projects               — проекты из PP_PROJECTS_ROOT ({name, path, git})
POST   /api/workflows              — создать draft workflow
POST   /api/workflows/validate-setup — read-only preflight мастера создания
GET    /api/workflows              — список workflow (?status=draft&limit=50)
GET    /api/workflows/{id}         — детали workflow
PATCH  /api/workflows/{id}         — обновить draft-метаданные с expected_version
POST   /api/workflows/{id}/start   — создать первый новый раунд
POST   /api/workflows/{id}/dispatch — поставить executor/reviewer task в очередь
POST   /api/workflows/{id}/gate    — записать результат ручного W1-гейта
POST   /api/workflows/{id}/review  — записать структурированный verdict аудитора
POST   /api/workflows/{id}/human-input — решение человека/возобновление
POST   /api/workflows/{id}/cancel  — отменить workflow и связанные задачи
POST   /api/workflows/{id}/sync    — восстановить проекцию из состояния tasks
POST   /api/workflows/{id}/history/import — импортировать старые раунды и факты
GET    /api/workflows/{id}/rounds  — раунды
GET    /api/workflows/{id}/rounds/{round_id}/runs — запуски ролей
GET    /api/workflows/{id}/events  — append-only история (?after_seq=0)
GET    /api/workflows/{id}/findings — текущие findings
GET    /api/workflows/{id}/artifacts — зарегистрированные артефакты

Конфигурация

Переменная По умолчанию Описание
PP_DATA_DIR ~/.promptpilot Директория для БД
PP_POLL_INTERVAL 5 Интервал опроса очереди (сек)
PP_TASK_TIMEOUT 0 (без лимита) Глобальный таймаут задачи, сек; у задачи переопределяется индивидуально
PP_BASE_DELAY 60 Начальная задержка retry (сек)
PP_MAX_DELAY 3600 Максимальная задержка retry (сек)
PP_MAX_RETRIES 5 Макс. кол-во retry по умолчанию
PP_CONCURRENCY 1 Сколько задач worker выполняет одновременно
PP_PIPELINE_SNAPSHOT_INTERVAL 300 Интервал фоновых снимков активных pipeline-профилей, сек; 0 отключает
PP_MIN_FREE_MB 0 (без проверки) Не начинать новую задачу, если свободно меньше памяти
PP_VERDICT 0 Просить агента заканчивать строкой ИТОГ: ... (дописывается к промпту)
PP_GUARD auto Сторож запретов: auto — при skip_permissions, 1 — всегда, 0 — выключен
PP_WORKTREE_PREFIX pp/ Префикс ветки задачи с worktree (pp/t42)
PP_WORKTREES_ROOT Куда класть чекауты; пусто = .pp-worktrees рядом с репозиторием
PP_WORKTREE_COPY .env Игнорируемые git'ом файлы, которые копировать в новый чекаут (через запятую; пусто — не копировать)
PP_DEFAULT_CLI claude Провайдер по умолчанию
PP_HOST 127.0.0.1 Хост веб-сервера
PP_PORT 8420 Порт веб-сервера
PP_API_TOKEN Токен авторизации Web UI/API (пусто = без авторизации)
PP_TG_TOKEN Токен Telegram бота
PP_TG_ALLOWED_PHONES Разрешённые номера (через запятую)
PP_TASK_PASSWORD Пароль для создания задач через бота
PP_PROJECTS_ROOT Корневая папка проектов для быстрого выбора директории
PP_CLAUDE_EXE из PATH Путь к claude / claude.exe
PP_HERDR_BIN herdr Путь к herdr CLI
PP_HERDR_KEEP_PANE 0 Форсировать «оставить сессию» независимо от галочки задачи
PP_HERDR_READ_LINES 300 Сколько строк транскрипта читать как результат
PP_HERDR_START_TIMEOUT_MS 60000 Таймаут готовности агента при старте сессии
PP_HERDR_WATCH 1 herdr→Telegram мост (уведомления о blocked/done)
PP_HERDR_WATCH_INTERVAL 10 Интервал опроса агентов herdr (сек)
PP_HERDR_RENOTIFY_COOLDOWN 600 Антидубль blocked-уведомлений: повтор с тем же экраном в течение этого срока молчит (сек)

Статусы задач

Статус Описание
pending В очереди
running Выполняется
completed Успешно завершена
failed Завершена с ошибкой
rate_limited Ожидает retry: rate limit или срыв по вине среды (см. ниже)
cancelled Отменена

Архитектура

promptpilot/
├── config.py       — настройки, провайдеры, скилы, build_cmd
├── models.py       — Pydantic-модели
├── db.py           — SQLite (очередь, CRUD, планирование)
├── worker.py       — воркер (subprocess → любой AI CLI)
├── cli.py          — CLI (Click)
├── api.py          — REST API (FastAPI)
├── bot.py          — Telegram бот (python-telegram-bot)
├── tg_auth.py      — авторизация по номеру телефона
└── static/
    └── index.html  — веб-интерфейс

start.ps1           — запустить все сервисы
stop.ps1            — остановить все сервисы
build.ps1           — собрать dist\pp.exe
pp.spec             — конфиг PyInstaller

Воркер и сервер — два отдельных процесса, работающих с одной SQLite БД. По умолчанию воркер выполняет задачи по одной; PP_CONCURRENCY>1 включает параллельное выполнение (см. раздел «Параллельные задачи»).

Лицензия

MIT — см. LICENSE.

About

Background task queue for Claude Code and other AI CLIs — web UI + Telegram bot

Topics

Resources

Stars

14 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages