Плагин Claude Code для автономного исполнения фазового плана в цикле: по одной фазе за отдельную интерактивную сессию, до полного выполнения плана либо до остановки по отсутствию прогресса. Каждая фаза идёт в свежей сессии — контекстное окно остаётся компактным.
-
/circle-skill:circle-skill <путь-или-имя-плана>— препролёт: находит план, нормализует формат (проставляет circle-маркеры, статусы выводит из «## Журнал»), классифицирует фазы по риску и спрашивает, какие рискованные/прод-фазы разрешить выполнять без тебя. -
Запускает фоновый цикл. На каждой итерации цикл сам выбирает следующую подходящую фазу, детерминированно собирает для неё компактный срез плана (
phase-context.md: read-once преамбула + текст фазы + журнал предыдущих фаз) и запускает свежую интерактивную сессиюclaudeпод PTY, которая выполняет ровно одну фазу, ведёт статус и дописывает журнал. Фазу со статусомdoneкоммитит сама сессия последним шагом —git add -A && git commitв репозитории плана (сообщениеcircle: phase <id> — <title>), хуки проекта уважаются (--no-verifyнигде нет). Так работа каждой завершённой фазы попадает в историю сразу, а откатgit reset --hardв заблокированной поздней фазе сбрасывает максимум к коммиту предыдущей, а не сносит весь накопленный прогресс. Смысл «сессия коммитит сама, а не цикл»: если pre-commit хук проекта (lint/test/format) отклонит коммит, тот же агент разбирается и устраняет причину в своём контексте — без обхода и без спавна новой сессии. Заблокированные/недоделанные фазы не коммитятся (они откатывают свою работу сами)..circle/в коммит не попадает (её*-gitignore).Цикл при этом сторожит инвариант «
done⇒ закоммичено»: признак коммита — сдвигHEADза сессию (а не чистота дерева: хук-форматтер вродеprettier --writeможет оставить residue уже после успешного коммита).HEADне сдвинулся, а дерево грязное → сессия забыла/не смогла закоммитить: цикл не теряет работу и не обходит — возвращает фазу вin_progressс obstacle, и следующая сессия дочинивает; непочиняемый случай (хук требует недоступного окружения/секрета) упирается в backstop и останавливает цикл (stuck), всё видно в summary. Если имени/почты для git нет нигде (ни в окружении, ни в конфиге), цикл дозаполняет недостающее поле fallback'омcircle-skill@local(настоящую identity не перекрывает). Push не делается никогда — публикация остаётся на проекте.Важно про область коммита: сессия делает
git add -Aв одном репозитории — том, что содержит план (circle рассчитан на однорепозиторное исполнение; для нескольких worktree есть отдельныйworktree-skill).add -Aфиксирует всё дерево этого репозитория, а не только диф фазы: любые посторонние незакоммиченные правки и любые файлы, которые сессия записала вне.circle/(включая случайные.env/дампы с секретами), попадут в коммит. Коммит локальный (пуша нет), но гони circle в репозитории, который не жаль так коммитить, — иначеCIRCLE_NO_COMMIT=1(сессии сказано не коммитить, сторож выключен). План вне git-репозитория → коммиты выключены. -
Останавливается, когда подходящих фаз не осталось (план исполнен) или план не изменился после сессии (нет прогресса) или сессия зависла (таймаут) или одна фаза выбиралась подряд слишком много раз без закрытия (застряла) или сессия завершилась аварийно — не записала result либо
run_phaseвернул ненулевой код (crash/error). Затем — финальный отчёт.
claude -p (headless) тарифицируется как программное использование (API). Плагин гоняет
настоящие интерактивные сессии под PTY — они идут по подписке, видят установленные плагины
и умеют спавнить субагентов.
claudeв PATH (логин по подписке;ANTHROPIC_API_KEYцикл намеренно сбрасывает).python3(только стандартная библиотека). macOS/Linux; Windows — через WSL.
claude plugin marketplace add https://github.com/SI-IC/circle-skill.git
claude plugin install circle-skill@circle-skillВерсия живёт в .claude-plugin/plugin.json и зеркалится в .claude-plugin/marketplace.json (значения обязаны
совпадать — это проверяет claude plugin tag). Версия семвер MAJOR.MINOR.PATCH.
Правило: каждый push ветки main поднимает версию. Релиз делается одной командой — она
поднимает версию в обоих манифестах, коммитит, пушит ветку и ставит тег circle-skill--vX.Y.Z:
./scripts/release.sh # patch (по умолчанию): 0.1.0 → 0.1.1
./scripts/release.sh minor # 0.1.3 → 0.2.0
./scripts/release.sh major # 0.4.2 → 1.0.0Правило навязывается git-хуком .githooks/pre-push: прямой git push origin main без поднятой
версии (≤ последнего релиз-тега) отклоняется. Хук активируется per-clone — после git clone
выполни один раз:
git config core.hooksPath .githooksРазовый обход (например, правка вне релиза): git push --no-verify.
Markdown. Фазы — секции ## Фаза <id> — <title> с маркером под заголовком:
## Фаза 2 — Перенос
<!-- circle: status=pending order=20 deps=[1] autonomy=auto obstacle="" -->
Поля: status (pending|in_progress|done|blocked|skipped), order, deps (id предшественников,
должны быть done), autonomy (auto|needs-human), obstacle. Плюс append-only секция ## Журнал:
каждая фаза дописывает в неё запись, а следующая получает её дайджест прямо в своём phase-context.md —
так контекст (что построено, какие verify-гейты слабые, предупреждения «следующий шаг») передаётся между
сессиями и не переоткрывается заново. Сама фаза при этом не читает весь план (часто 100+ КБ), а только
свой срез — меньше токенов и времени на холодный старт.
Чтобы фоновые сессии не тратили время на повторную разведку кодовой базы, план несёт результат
разведки, снятой один раз при планировании: секцию ## Карта кодовой базы в преамбуле (навигационный
индекс — где живут подсистемы, entrypoints, конвенции; попадает в срез каждой фазы) и файловый
манифест в теле каждой фазы (какие файлы фаза создаёт/меняет, с привязкой по символам/функциям, а не
номерам строк). Executor берёт манифест как стартовую карту — открывает эти файлы сразу, а не ищет
вслепую. Как это авторить — см. circle-plan-authoring.md в плагине main-skill.
Цикл пишет рантайм рядом с планом, в отдельную папку на каждый план:
.circle/<имя-плана>/ (loop.log, summary.txt, result, executor-prompt.md, phase-context.md,
lock.d). Планы в одной директории не делят файлы и не затирают логи друг друга.
Вся .circle/ гарантированно вне git: цикл кладёт туда .gitignore с *. Логи несут сырой вывод
сессий — там возможны секреты/чувствительные данные, коммитить их нельзя. loop.log пишется уже
очищенным от TUI-перерисовок (снят ANSI, схлопнуты подряд идущие одинаковые кадры спиннеров) — он
в разы компактнее сырого PTY, но сохраняет весь нарратив сессии и пригоден для пост-мортем-анализа.
Прогресс живой фазы виден по heartbeat-строкам, которые цикл пишет в loop.log раз в ~5 с:
CIRCLE_PROGRESS: контекст N% · фаза <id> — N% это потребление контекстного окна сессией (из
нижнего статус-бара TUI; нет бара с % → контекст ?). По ним под строкой «выбрана фаза X» сразу
видно, что сессия жива и сколько контекста съела. Быстрый просмотр: grep CIRCLE_PROGRESS loop.log
(или tail -f loop.log | grep --line-buffered CIRCLE_PROGRESS).
CIRCLE_IDLE_TIMEOUT— адаптивный детектор зависания по молчанию PTY (сек, по умолч. 1800 = 30 мин;0= выкл). Живой claude непрерывно тикает статус-баром TUI — в том числе пока крутится долгий тул (тест/сборка/установка), — поэтому «ноль вывода PTY дольше idle» означает, что процесс завис, а не что фаза просто долгая. Меряется тишина, не длительность тула: 20-минутный прогон тестов idle не рвёт, потому что бар всё это время тикает. Сработал раньшеCIRCLE_TIMEOUT→ стопhang; причина пишется вloop.logстрокойCIRCLE_PHASE_END: idle-timeout ….CIRCLE_TIMEOUT— абсолютный потолок wall-clock сессии (сек, по умолч. 14400 = 4ч) — страховка от runaway; ставится большим, чтобы не рубить здоровую-но-долгую фазу (её зависание ловит idle выше). Жёсткий: по SIGALRM прерывает даже заблокированный syscall и гарантированно убивает сессию (всю process-группу) заtimeout+~8s, не полагаясь на штатное завершение claude. Стопhang, причина вloop.log—CIRCLE_PHASE_END: wall-timeout ….CIRCLE_MAX_SAME_PHASE— сколько раз подряд одна фаза может выбираться без закрытия до стопа «застряла» (по умолч. 3).CIRCLE_NO_COMMIT— если задан (1), сессиям сказано не коммитить и guard инварианта выключен (по умолч. коммит делает сама сессия — см. «Как работает»). Для проектов, где коммитами управляет что-то другое.CIRCLE_PYTHON— python-интерпретатор (по умолч.python3).CIRCLE_CLAUDE_BIN— бинарь claude (по умолч.claude).CIRCLE_CLAUDE_MODEL— модель для фазовых сессий (--model, по умолч. пусто → claude берёт свой обычный дефолт). Препролёт-команда (/circle-skill:circle-skill) выставляет её автоматически в модель самой себя, так что план исполняется на той же модели, на которой был запущен. Формат проверяется до старта цикла (иначе стопcrashдо первой фазы); читается один раз при запуске — устарела/отозвана модель посреди многочасового прогона → следующая фаза упадёт тем жеcrash, чинить руками (сменить/снять переменную и перезапустить цикл).ANTHROPIC_API_KEY— намеренно сбрасывается циклом перед запуском сессий (принудительно подписка, не API). Если ключ задан глобально — на фазовые сессии он не повлияет.
Цикл может писать безопасную структурную статистику каждого прогона, чтобы LLM-аналитик по команде владельца предлагал, где план тормозит и как ускорить плагин. По умолчанию off. Потребитель — LLM, не человек-с-дашбордом.
На фазу собираются числа/enum/флаги: duration_s, attempts, outcome, files_changed,
subphases_added, deps_count, autonomy, context_pct (пик потребления контекстного окна
сессией, % — null, если статус-бар не распознан), ctx_parse_failed (дрейф извлечения контекста)
и manifest_miss_count (промахи манифеста).
context_pct — независимая от duration_s ось: время не отличает пухлую фазу от просто медленной,
а пик контекста отличает. ctx_parse_failed разводит два вида context_pct=null: True = бар-подобные
кадры были, но _CTX_RE их не сматчил (чинибельный дрейф формата TUI), False = бара не было вовсе
(headless/тихая сессия), null = run_phase не отчитался. Агрегат True по многим фазам ⇒ пора чинить
_CTX_RE. manifest_miss_count — сколько раз сессия отчиталась о расхождении
манифеста с реальностью (сигнал стоимости «въезда» в фазу): self-report через токен ok/miss(N),
null = сессия не отчиталась (отличимо от отчитанного 0). Собирается self-report'ом, а НЕ path-diff'ом
(declared-vs-touched по прозе манифеста был в схеме v1 — всегда 0, регэксп не парсил прозу; удалён).
Прогон завершается со stop_reason: complete (остаток плана — только done/skipped/pending
needs-human: цикл отработал автономный мандат) или stalled (остались blocked или застрявшие за
зависимостью auto-фазы — план НЕ доведён), плюс аварийные hang/stuck/no-progress/crash/error.
stalled отделён от complete намеренно: иначе застрявший на блокере прогон завышал бы долю доведённых.
Как читать записи (чтобы не сделать ложный вывод):
phases[]— фазы, РЕАЛЬНО исполненные в этом прогоне;phases_totalиstatus_counts— финальное состояние ВСЕГО плана.len(phases) < phases_total— норма, а не потеря данных: уже-doneдо старта иskippedфазы сессиями не исполняются и пофазной записи не имеют.context_pct=nullпри БОЛЬШОМduration_s(живой TUI рисует бар непрерывно) ⇒ извлечение сломано (дрейф формата статус-бара), а не «низкая нагрузка». Тогдаnullреально попадает в запись (поле НЕ выкидывается — осмысленное «измерено, но неизвестно», отличимо от «поля не было» в старых схемах поschema_version). Причинуnullподсказываетctx_parse_failed:True⇒ дрейф_CTX_RE(бар был),False⇒ бара не было (headless). В сессии с дрейфомrun_phaseкладёт вrun-stats/ctx-unparsed.logстрокиCIRCLE_CTX_UNPARSED: …с образцом нераспознанного кадра — по ним чинится регэксп_CTX_RE(файл локальный, под gitignore, копится за прогон; в телеметрию образцы не уходят).- Простой прогона (паузы/resume) =
run_wall_s − Σ duration_s; отдельного поля под это нет.
Приватность — структурная, by construction. В запись попадают только числа, булевы, enum из
закрытых словарей и HMAC-хеши. Ни путей, ни кода, ни текста ошибок, ни obstacle, ни имён
проекта/фаз — их там нет по построению схемы. Приёмник повторно прогоняет fail-closed скраб и
роняет любую запись с подозрительной строкой. Токен и данные никогда не в VCS (.env и
.telemetry/ в .gitignore, pre-commit guard .githooks/pre-commit).
Ноль лишних токенов в проектах. Весь сбор и отправка — детерминированные (bash+git+python),
без участия LLM-сессии. Единственное «дорогое» — финальная строка телеметрия: отправлено=N …,
и её печатает цикл, не модель.
bash scripts/telemetry-server-install.sh # systemd-сервис, порт 3000, переживает рестартСкрипт генерирует сильный bearer-токен в .env (если его там нет), поднимает telemetry_server.py
как системный circle-telemetry.service и печатает токен для проектов. Store входящих —
.telemetry/runs/ (под gitignore). Эндпоинт доступен по публичному preview-URL контейнера.
Вставь URL приёмника и токен и проверь связь одной командой (пишет их в .env проекта, .env
добавляется в .gitignore, затем пинг /health):
/circle-skill:circle-telemetry activate https://<preview-url> <токен>
Обновился токен на приёмнике — повтори activate с новым (перезапишет в .env). Приёмник был
недоступен на финише прогона — записи ждут в outbox, догоняются на следующем прогоне или командой
/circle-skill:circle-telemetry send.
| Env | Где | Смысл |
|---|---|---|
CIRCLE_TELEMETRY_URL |
.env проекта |
адрес приёмника; нет → телеметрия off |
CIRCLE_TELEMETRY_TOKEN |
.env |
bearer; нет → off. НИКОГДА не в VCS |
CIRCLE_TELEMETRY_SALT |
.env |
секрет HMAC-идентификаторов; нет → anon (без кросс-машинной группировки) |
CIRCLE_TELEMETRY_STORE |
приёмник | папка входящих (по умолч. .telemetry/runs) |
CIRCLE_TELEMETRY_PORT |
приёмник | порт (по умолч. 3000) |
Локальная команда /circle-analyze — front-door разбора: дёргает fresh, отдаёт свежие записи
Claude на анализ и выдаёт рекомендации двумя корзинами (эффективность цикла / улучшение сбора);
внедрение правок и mark — только после твоего одобрения. Команда лежит в .claude/commands/
приёмного контейнера (gitignored) и в пакет плагина не входит — в проектах, где плагин используется,
её нет. Канонический источник — scripts/circle-analyze.command.md (коммитится, но командой не
становится: команды берутся только из commands/ и .claude/commands/); telemetry-server-install.sh
разворачивает его в .claude/commands/, так что команда переживает пересоздание контейнера.
Под капотом — тот же ledger: python3 scripts/telemetry_analyze.py fresh показывает, какие записи
ещё не разобраны, python3 scripts/telemetry_analyze.py mark отмечает их разобранными (эти же шаги
можно вызвать вручную, без команды).
python3 -m unittest discover -s tests -vЮнит/интеграционные тесты используют поддельный claude. Для проверки на реальной сессии:
создай минимальный план с одной auto-фазой (например «создай файл hello.txt с текстом hi»),
запусти /circle-skill:circle-skill <plan>, подтверди (рискованных фаз нет) и убедись, что фаза выполнена,
статус done, журнал дописан, цикл остановился с complete.