Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

69 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

circle-skill

Плагин Claude Code для автономного исполнения фазового плана в цикле: по одной фазе за отдельную интерактивную сессию, до полного выполнения плана либо до остановки по отсутствию прогресса. Каждая фаза идёт в свежей сессии — контекстное окно остаётся компактным.

Как работает

  1. /circle-skill:circle-skill <путь-или-имя-плана> — препролёт: находит план, нормализует формат (проставляет circle-маркеры, статусы выводит из «## Журнал»), классифицирует фазы по риску и спрашивает, какие рискованные/прод-фазы разрешить выполнять без тебя.

  2. Запускает фоновый цикл. На каждой итерации цикл сам выбирает следующую подходящую фазу, детерминированно собирает для неё компактный срез плана (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-репозитория → коммиты выключены.

  3. Останавливается, когда подходящих фаз не осталось (план исполнен) или план не изменился после сессии (нет прогресса) или сессия зависла (таймаут) или одна фаза выбиралась подряд слишком много раз без закрытия (застряла) или сессия завершилась аварийно — не записала result либо run_phase вернул ненулевой код (crash/error). Затем — финальный отчёт.

Почему интерактивные сессии, а не claude -p

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).

Настройки (env)

  • 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.logCIRCLE_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-переменные телеметрии

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

Ручной smoke (реальная сессия, под подпиской)

Юнит/интеграционные тесты используют поддельный claude. Для проверки на реальной сессии: создай минимальный план с одной auto-фазой (например «создай файл hello.txt с текстом hi»), запусти /circle-skill:circle-skill <plan>, подтверди (рискованных фаз нет) и убедись, что фаза выполнена, статус done, журнал дописан, цикл остановился с complete.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages