A task carrying the Plane `Bug` label takes a shortened route that skips the `architecture` stage (one opus architect run + ADR + check_architecture_done), replacing heavy analysis with a lite package (bug-report + mandatory regression test plan). EVERY Quality Gate / sub-gate runs UNCHANGED — the route is a scheduler property, not a gate (root invariant NFR-1): STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys are byte-for-byte preserved. - src/bug_fast_track.py: new leaf (never-raise) — bug_fast_track_applies (local, network-free, checked first), is_bug_task (labels.has_label, Plane API source), skips_architecture (pure DB-backed routing predicate), snapshot. - src/db.py: additive idempotent tasks.track column (TEXT DEFAULT 'full') + set_task_track / get_task_track helpers (missing/NULL -> 'full', fail-safe). - src/stage_engine.py: routing-override on the analysis-exit edge (track='bug' -> development/developer, skipping architect); brd-review-clock stamp extended to analysis->development. get_next_stage/get_agent_for_stage stay pure. - src/webhooks/plane.py: classify task as bug in start_pipeline (applies-first short-circuit; never-raise -> full cycle on any error). - src/main.py: additive bug_fast_track block in GET /queue + POST /bug-fast-track/escalate (reset 'bug'->'full' to return to the full cycle). - src/config.py: bug_fast_track_enabled / _label / _repos flags (empty CSV -> self-hosting only). - src/notifications.py: optional 🐞 marker on the bug-track card (never-raise). - Prompts: analyst.md (lite bug package + escalation), reviewer.md (regression- test axis) — 52d canon preserved. - Docs: CLAUDE.md, README.md (env + API + section), docs/architecture/README.md, CHANGELOG.md, .env.example. - Tests: tests/test_bug_fast_track*.py + test_db_migrations.py + queue block (TC-01..TC-15). Full regression green (1551 passed). Kill-switch ORCH_BUG_FAST_TRACK_ENABLED=false -> 1:1 pre-ORCH-019 (zero regression; residual track column harmless). Refs: ORCH-019 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
277 lines
42 KiB
Markdown
277 lines
42 KiB
Markdown
# CLAUDE.md — паспорт проекта orchestrator
|
||
|
||
## TL;DR
|
||
Мульти-агентный оркестратор разработки. FastAPI-сервис: принимает webhooks от Plane и Gitea, ведёт задачи по конвейеру стадий через Quality Gates, запускает Claude CLI агентов (analyst → architect → developer → reviewer → tester → deployer) на каждой стадии. **Оркестратор дорабатывает в том числе сам себя (self-hosting).**
|
||
|
||
## Стек
|
||
- Backend: FastAPI + uvicorn (Python 3.12)
|
||
- БД: SQLite (`src/db.py`)
|
||
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`. **ORCH-74:** модель/эффорт агента берутся ТОЛЬКО из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41) — frontmatter `model:` удалён как мёртвый, frontmatter описательный; имя модели валидируется форматом `^claude-…$` перед `--model` (never-break). **ORCH-077 (52d, замыкает эпик 52):** тело всех 6 промптов переписано в едином **каноне Anthropic** (5 обязательных XML-секций в нормативном порядке `<context>`→`<task>`→`<deliverables>`→`<constraints>`→`<output_format>`, запреты в формате «❌ X → ✅ Y», `<thinking>` у решающих ролей), и каждый промпт **добровольно** эмитит 6-польную frontmatter-схему 52c (`work_item`/`stage`/`author_agent`/`status`/`created_at`/`model_used`) **аддитивно** — рядом с machine-verdict ключом, НЕ меняя его имя/регистр/значения (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:` — байт-в-байт). Это **docs/prompts-only** изменение: `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты; `frontmatter_validation_strict` остаётся `False` (enforcement НЕ включён). Промпт `cat`-ается из worktree в момент запуска → новые промпты вступают в силу на следующем worktree от `main` без прод-рестарта. Анти-регресс — структурные тесты `tests/test_agent_prompts_canon.py` + зелёный `test_agent_frontmatter_no_model.py`. **Норматив на будущее:** новые/изменённые агент-промпты следуют этому канону. Детали — `docs/architecture/adr/adr-0021-prompt-canon-anthropic.md`. **ORCH-092 (эпилог эпика 52, docs/prompts-only):** аудит 6 промптов поверх канона — копируемые frontmatter-примеры расхардкожены (`created_at: <YYYY-MM-DD>`/`model_used: <resolve ORCH-41>` + врезка «подставь `date +%F`/модель из конфига, не копируй буквально»; литерал `claude-opus-4-8` — только справка в таблице полей); добавлена секция `<escalation>` developer/reviewer/tester (после `</success_criteria>`, порядок 5 секций цел); developer лишён ручного `git rebase origin/main` (свежесть базы — инвариант движка serial-gate ORCH-088 + `auto_rebase_onto_main` под merge-lease; ручной rebase конфликтовал с запретом force-push — ADR-001 D1); tester обогащён worktree-путём + smoke `serial_gate` + покрытием каждого TC; из reviewer удалена мёртвая строка «тот же экземпляр Developer». **Языковое исключение (нормативно, ADR-001 D2):** `deployer.md` сознательно остаётся на **английском** (5 ru + 1 en) как самый safety-critical промпт — НЕ «чинить» язык вслепую; критичные self-hosting-запреты подняты в видную рамку. Verdict-ключи и канон 52d — байт-в-байт; анти-регресс — `tests/test_agent_prompts_canon.py` (ORCH-092 TC-01…TC-08). Детали — `docs/work-items/ORCH-092/06-adr/ADR-001-developer-rebase-and-deployer-language.md`.
|
||
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1). **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота `max_concurrency`; декларации/детект циклов — leaf `src/task_deps.py` (kill-switch `ORCH_TASK_DEPS_ENABLED`). Сериализация мержа одного репо — безусловный pre-merge rebase под merge-lease (`ORCH_PREMERGE_REBASE_ALWAYS`). **ORCH-088 (serial gate, Этап 1):** новая задача репо не входит в `analysis` (analyst-job не выбирается, ветка не режется), пока в репо есть **более ранняя** незавершённая задача (`t2.id < jobs.task_id`, FIFO) ИЛИ репо заморожен (`repo_freeze`). Срез ветки **отложен** со `start_pipeline` на момент claim analyst-job (`launcher._materialize_deferred_branch`) — база = свежий `origin/main` с кодом предшественника (анти-stale-base). Post-deploy `DEGRADED` → durable per-repo freeze (`repo_freeze`, `cleared_at IS NULL` = активен) + Telegram; снятие — вручную `POST /serial-gate/unfreeze?repo=…`. Leaf `src/serial_gate.py` (claim — fail-OPEN, freeze — fail-CLOSED); флаги `ORCH_SERIAL_GATE_ENABLED` (kill-switch), `ORCH_SERIAL_GATE_REPOS` (CSV; пусто = все репо), `ORCH_SERIAL_GATE_FREEZE_ENABLED`. Блок `serial_gate` в `GET /queue`. `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты. **ORCH-093 (merge-актор устойчив к икоте Gitea):** детерминированный merge-актор под-гейта `deploy → done` (`src/merge_gate.py`) ретраит **транзиентные** ошибки Gitea вместо ложного HOLD (инцидент ORCH-063: `POST …/merge` → `405 "try again later"` сразу после пуша). `merge_pr` оборачивает **только** мутирующий `POST …/merge` в ограниченный retry-loop с экспоненциальным backoff (`min(base*2^(i-1), max)`, потолок суммарного сна `(N-1)*max ≤ 10 с`); классификатор `_classify_merge_response`: транзиент (ретрай) — `405`/`408`/`5xx`/таймаут/сетевая + `409`/`422` при `mergeable==True` (доп. `GET /pulls/{index}`; `mergeable==None` → дефолт-транзиент, fail-OPEN-в-ретрай), терминал (быстрый честный `False`, защита ORCH-071/073 как прежде) — `403`/`404`/реальный конфликт (`mergeable==False`). Kill-switch `merge_retry_enabled=false` → ровно один POST (байт-в-байт прежнее one-shot); флаги `ORCH_MERGE_RETRY_*` (`max_attempts=3`, `backoff_base_s=2`, `backoff_max_s=5`). Гард **already-in-main** в `ensure_open_pr` (leaf `_branch_fully_in_main`, `git merge-base --is-ancestor HEAD origin/main`): ветка целиком в `main` → исход `"already-in-main"` без создания мусорного пустого PR; `_handle_merge_verify` пропускает `merge_pr` и отдаёт авторитетному SHA-в-main довести до `done` (НЕ HOLD); git-ошибка → fail-OPEN на create-путь. Без отдельного флага (накрыт `merge_verify_autocreate_pr_enabled`). INV-4 (мерж только через Gitea PR-merge API, никогда push/force-push в `main`), never-raise, `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — сохранены. Детали — `docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`, сквозной `docs/architecture/adr/adr-0027-merge-actor-transient-retry-and-already-in-main.md`.
|
||
- Контейнеризация: Docker + Compose
|
||
- CI/CD: Gitea Actions (`.gitea/workflows/`)
|
||
- Деплой: docker compose на mva154
|
||
|
||
## Команды
|
||
- `uvicorn src.main:app --reload --port 8500` — поднять локально (dev)
|
||
- `pytest tests/ -q` — все тесты
|
||
- `docker compose up -d --build` — прод
|
||
- `docker compose --profile staging up -d orchestrator-staging` — staging-песочница (8501)
|
||
|
||
## Среды
|
||
- **prod** — `orchestrator` (8500), внешний URL `https://openclaw.mva154.duckdns.org/orchestrator/`
|
||
- **staging** — `orchestrator-staging` (8501), изолированная БД (`./data/staging`), только sandbox-проект
|
||
|
||
## Структура
|
||
- `src/` — приложение (main, config, db, stages, stage_engine, queue_worker, projects, usage)
|
||
- `src/agents/launcher.py` — запуск Claude CLI агентов
|
||
- `src/qg/checks.py` — Quality Gate проверки
|
||
- `src/webhooks/` — приём вебхуков Plane/Gitea
|
||
- `tests/` — pytest
|
||
- `docs/` — документация, ADR, work-items, operations
|
||
- `scripts/` — утилиты (staging_check.py, orchestrator-deploy-hook.sh)
|
||
|
||
## Конвейер (кратко; детали — docs/architecture/README.md)
|
||
```
|
||
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
|
||
↑ │
|
||
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3)
|
||
```
|
||
|
||
## Статусная модель Plane (ORCH-066) — индикация ≠ управление
|
||
Статусы Plane — это **слой B (индикация)**, отдельный от **слоя A (машина стадий)** `src/stages.py::STAGE_TRANSITIONS`. Plane показывает наблюдателю осмысленную картину (`Backlog → Todo → Analysis → Architecture → Development → Code-Review → Testing → Awaiting Deploy → Deploying → Monitoring after Deploy → Done` + человеческие гейты `In Review/Approved`, `Confirm Deploy`), но НИКОГДА не управляет конвейером. Маппинг и сеттеры — `src/plane_sync.py` (6 новых ключей: `to_analyse/analysis/code_review/awaiting_deploy/deploying/monitoring`), с project-relative alias-fallback: на частично сконфигурированном проекте новый ключ деградирует на базовый UUID ТОГО ЖЕ проекта (нулевая регрессия для enduro-trails). Детали — `docs/architecture/README.md`.
|
||
|
||
**Terminal-window-aware гард deploy-статусов (ORCH-094).** Задача с БД `stage=done` и 0 активных job'ов стабильно держит Plane=`Done`: три deploy-фазовых сеттера (`set_issue_awaiting_deploy`/`set_issue_deploying`/`set_issue_monitoring`) были терминал-слепы и флаппили `Awaiting ⟷ Monitoring` (верифицировано на ORCH-061, task 47), т.к. любой стейл/двойной/неизвестный вызов под бот-токеном перезаписывал терминал промежуточным статусом. Новый leaf `src/deploy_status_guard.py` (чистая, never-raise, config-gated; по образцу `serial_gate`/`labels`/`cancel`) — `decide(work_item_id, target, reason) -> ALLOW | CONVERGE_DONE | SUPPRESS` на **входе** трёх сеттеров `plane_sync` (низкий чокпоинт ловит любой путь, включая неизвестный актор). Инвариант: deploy-статус легитимен ⇔ задача **нетерминальна** ИЛИ (`done` И активно пост-деплой-окно `post_deploy.window_active` = ARMED & не DONE); иначе для `done` — идемпотентное `CONVERGE_DONE` (сеттер зовёт `set_issue_done`), для `cancelled` — `SUPPRESS`. Чтобы легитимный первый `Monitoring` (БД уже `done` к моменту стр. 404) прошёл, арм-блок `post_deploy.arm_monitor` **перенесён выше** terminal-sync-блока в `advance_stage` (ADR-001 D3) → `window_active==True` до выставления `Monitoring`. Монитор-тик при БД `cancelled` мид-окно → закрыть окно без статус-PATCH (zombie-tick guard, FR-3). Наблюдаемость: BC-kwarg `reason` у трёх сеттеров + одна структурная лог-запись на вердикт (`work_item`/`caller`/`target`/`db_stage`/`window_active`/`verdict`; converge/suppress → WARNING). Read-only аксессор `db.get_task_by_work_item_id`. Флаги `deploy_status_guard_enabled` (kill-switch; `False` → 1:1 прежнее) / `deploy_status_guard_repos` (CSV; **пусто → self-hosting only**, enduro не затронут). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты. Детали — `docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`, сквозной `docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md`.
|
||
|
||
## Нотификации / Telegram live-tracker (ORCH-042/066/067/087)
|
||
Каждая задача = **одна карточка** в Telegram (`src/notifications.py`). Поведение карточки:
|
||
- **Дефолт `tracker_mode` — `bump`** (ORCH-067; `edit` доступен через `ORCH_TRACKER_MODE=edit`).
|
||
`bump` на каждом обновлении удаляет старую карточку и шлёт свежую вниз чата (тихо), `edit`
|
||
редактирует на месте. Инвариант «одна карточка на задачу» — в обоих режимах.
|
||
- **Зачистка сирот (ORCH-087):** bump ведёт авторитетный леджер ВСЕХ созданных карточек
|
||
(таблица `tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении удаляет
|
||
ВСЕ незакрытые mid, а не только скаляр `tracker_message_id` (он сохранён как указатель на
|
||
текущую карточку, BC). Это устраняет класс «замёрзшая сирота» (старая карточка с заголовком
|
||
ранней стадии, потерявшая ссылку при гонке/`delete`-fail+`send`-ok). Новый mid пишется в
|
||
леджер ТОЛЬКО при успешном `send` (BR-6); transient-`delete` остаётся незакрытым для ретрая;
|
||
«already gone»/>48ч (`_DELETE_GONE_MARKERS`) → закрывается. Остаточная гонка самозалечивается
|
||
за один bump. Known-limitation: Telegram 48ч (сироты старше неудаляемы).
|
||
- **Эффорт в строке стадии (ORCH-087):** колонка `agent_runs.effort` стампится фактическим
|
||
`resolve_agent_effort` в `launcher._spawn` (CLI его в result-JSON не возвращает); строка
|
||
рендерится `· {model} · {effort}` (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`);
|
||
пустой/исторический effort → суффикс опускается.
|
||
- **Честное итоговое время (ORCH-087):** done-строка = три независимых подписанных метрики
|
||
`⏱️ Агенты {Σ agent_runs} · твоё {review~cap} · общее с ожиданием {wall}` (раньше `Всего {wall}`
|
||
читалось как сумма, которой не является). «Твоё» ограничено `tracker_brd_review_cap_s`
|
||
(`ORCH_TRACKER_BRD_REVIEW_CAP_S`, дефолт 2ч; маркер `~` при отсечке аномального застоя).
|
||
- **Статус-строка карточки** (`📍 <status_label>`) показывает текущий Plane-статус по модели
|
||
ORCH-066 (`plane_status_label`). Оффлайн-ядро (`stage → статус`, In Review из brd-clock)
|
||
работает всегда без сети; best-effort live-overlay (kill-switch `tracker_live_status`,
|
||
TTL-кэш, короткий таймаут) лишь дорисовывает ветки, неотличимые offline (Needs Input /
|
||
Blocked / Rejected / Cancelled / **Confirm Deploy** / Deploying / Monitoring) и **никогда не
|
||
блокирует конвейер**.
|
||
- **Кликабельный номер задачи** (`plane_issue_link`) — `ORCH-NNN` в карточке И во всех
|
||
уведомлениях (`notify_*`, alert'ы стадий) рендерится как `<a href=…>` на issue в Plane;
|
||
fail-safe → просто `html.escape(номер)`, если ссылку построить нельзя. Никогда не падает.
|
||
- **Без link-preview (ORCH-080):** оба примитива (`send_telegram`/`edit_telegram`) шлют
|
||
payload с `disable_web_page_preview: True` — баннер Plane («Modern project management»)
|
||
под кликабельной ссылкой `ORCH-NNN` больше не разворачивается ни в карточке (`bump`/`edit`),
|
||
ни в notify/alert-сообщениях. `parse_mode: HTML` сохранён → ссылка остаётся кликабельной.
|
||
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
|
||
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
|
||
|
||
## Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089)
|
||
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон
|
||
(эпик ORCH-088): гейт BRD (`analysis`: ручной `Approved`) и гейт прод-деплоя
|
||
(`deploy` Phase A: ручной `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти
|
||
два человеческих решения** — выборочно (лейбл Plane на задаче), декларативно,
|
||
обратимо, **не трогая ни одной технической проверки**. Инвариант: авто-режим снимает
|
||
лишь ожидание человеческого сигнала; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД
|
||
— **не трогаются**. Аддитивно: leaf `src/labels.py` (never-raise) + две точечные врезки.
|
||
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
|
||
`files_ok`): `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент +
|
||
`advance_stage(..., finished_agent=None)` — **тот же путь, что человеческий Approved**
|
||
(`approved-via-status` → `analysis → architecture` + `mark_brd_review_ended`).
|
||
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` после advance
|
||
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b`
|
||
(маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
|
||
индикативно-человеческие шаги. **BR-5 структурно:** Phase A достигается только после
|
||
зелёных под-гейтов ребра `deploy-staging → deploy` (security → merge-gate →
|
||
image-freshness → staging) → autoDeploy физически не деплоит сломанное.
|
||
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (`None` при ошибке ≠ `[]`) +
|
||
`get_project_labels` (`{normalized_name→uuid}`, TTL-кэш); сопоставление по
|
||
нормализованному имени (`strip().casefold()`), неоднозначность → «нет лейбла».
|
||
Источник истины — Plane API, не payload вебхука. Новый сеттер `set_issue_approved`.
|
||
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
|
||
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
|
||
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
|
||
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед.
|
||
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
|
||
«нет авто» → ручной гейт (never-raise). Прозрачность: лог + Telegram + Plane-коммент +
|
||
live-карточка; блок `auto_labels` в `GET /queue`. **Инфра-предусловие:** создать лейблы
|
||
`autoApprove`/`autoDeploy` в Plane-проекте ORCH (их отсутствие = ручной режим, fail-safe).
|
||
Детали — `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
|
||
`docs/architecture/adr/adr-0018-auto-label-gates.md`.
|
||
|
||
## Отмена задачи: статус STOP (ORCH-090)
|
||
Выделенный Plane-статус **STOP** — операторская кнопка «отменить + сбросить» задачу. Вводит
|
||
**новое системное терминальное состояние `cancelled`** (стадия `tasks.stage='cancelled'` + job-исход
|
||
`jobs.status='cancelled'`), равноправное `done`. Логический ключ `stop` — **fail-closed** (нет в
|
||
`_DEFAULT_STATES`, по образцу `confirm_deploy`/ORCH-059): доска без статуса STOP → ветка не
|
||
активируется. Маршрут `handle_issue_updated → handle_stop → stage_engine.cancel_task`:
|
||
- **Полный сброс** (вне критичного окна): graceful SIGTERM активного агента (`launcher.stop_process`,
|
||
переиспользует каскад `_watchdog`), все job'ы → терминальный `cancelled` (не реквью'ятся:
|
||
`claim_next_job` берёт только `queued`, reaper/worker сверяют терминал задачи — TR-2), удаление
|
||
worktree + **рабочей** Gitea-ветки (`gitea.delete_remote_branch`, **никогда** `main`, без
|
||
force-push), durable `stage='cancelled'` + **тумбстон** натуральных ключей (`plane_id`/
|
||
`work_item_id`/`plane_issue_id` → суффикс `#cancelled-<id>`; ADR-001 D4 уточнён: тумбстонится и
|
||
`plane_issue_id`, т.к. `get_task_by_plane_id`/`create_task_atomic` матчат по нему — иначе re-create
|
||
коллизирует; исходный UUID парсится из суффикса для аудита). Docs-артефакты (`01..17`) сохраняются.
|
||
- **STOP в критичном окне merge/deploy** (ADR-001 D7): `cancel.in_critical_window` → **отложенная**
|
||
отмена: `tasks.cancel_requested_at`, снимаются только `queued` job'ы (running-актор деплоя/мержа не
|
||
трогается), алерт; детерминированный finalizer (`run_deploy_finalizer`) доводит необратимый шаг до
|
||
честного исхода и применяет отмену (`force=True`). «Критичное окно» = реально начатый необратимый
|
||
шаг: INITIATED-sentinel self-deploy (ORCH-036; детач-деплой + поздний `merge_pr` в
|
||
`_handle_merge_verify` идут под тем же маркером) **либо** держание merge-lease (ORCH-043) **И**
|
||
активно бегущий актор (running-job). **Уточнение P1 (ORCH-090 review):** держание merge-lease в
|
||
Phase A на стадии `deploy` в ожидании ручного `Confirm Deploy` БЕЗ бегущего актора **полностью
|
||
обратимо** (ничего не смержено/задеплоено) → НЕ критично → немедленный полный сброс (сам отпускает
|
||
lease). Иначе отмена откладывалась бы к finalizer'у, который оператор (нажавший STOP именно чтобы НЕ
|
||
подтверждать деплой) не запускает — задача застревала бы с удержанным lease, клиня serial-gate репо.
|
||
STOP **никогда** не трогает `main`/force-push/прод-контейнер/detached-процесс (NFR-3).
|
||
- **Кросс-каттинг (adr-0026):** предикат «задача терминальна» расширен `{done}` → `{done, cancelled}`
|
||
в `serial_gate`/`task_deps`/`stages.py`-сток (иначе отменённая задача заклинит очередь репо);
|
||
reconciler-терминал-скип уже знал `cancelled` (ORCH-086). `STAGE_TRANSITIONS` exit-гейты рёбер /
|
||
`QG_CHECKS` / `check_*` — **не тронуты** (`cancelled` — сток, не ребро).
|
||
- **Дыра релонча закрыта (D6):** relaunch агента в `handle_status_start` ограничен стадией `analysis`
|
||
(единственный владелец Needs Input, ORCH-066); ручной перевод существующей задачи в иной промежуточный
|
||
статус больше не релончит середину пайплайна. Запуск пайплайна — только «To Analyse» → `start_pipeline`.
|
||
- Флаги `stop_status_enabled` (kill-switch; `False` → всё инертно, нулевая регрессия) / `stop_status_repos`
|
||
(CSV; пусто → все репо). Leaf `src/cancel.py` (never-raise). Read-only блок `stop` в `GET /queue`.
|
||
Аддитивные колонки `tasks.cancelled_at`/`cancel_requested_at` (`_ensure_column`). **Инфра-предусловие:**
|
||
создать статус **STOP** с группой `cancelled` на доске ORCH (его отсутствие = fail-safe no-op). Детали —
|
||
`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`,
|
||
`docs/architecture/adr/adr-0026-stop-cancel-task.md`.
|
||
|
||
## Багфикс-трек: дешёвый маршрут для багов (ORCH-019)
|
||
Задача с меткой Plane `Bug` идёт **укороченным маршрутом** — пропускается стадия `architecture`
|
||
(отдельный прогон opus-агента `architect` + ADR + exit-гейт `check_architecture_done`); тяжёлая
|
||
аналитика заменяется облегчённым пакетом (короткий bug-report + обязательный план регресс-теста,
|
||
но всё равно все 4 файла analysis — гейт `check_analysis_complete` не меняется). **Корневой
|
||
инвариант (NFR-1):** срезается ТОЛЬКО аналитика/архитектура — **все Quality Gate'ы и под-гейты
|
||
исполняются без изменений** (`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи —
|
||
байт-в-байт прежние); маршрутизация багфикса — свойство планировщика, **не** гейт. Аддитивно, под
|
||
kill-switch, never-raise, fail-safe → полный цикл.
|
||
- **Классификация (D1):** leaf `src/bug_fast_track.py` (never-raise, образец `labels`/`serial_gate`).
|
||
`bug_fast_track_applies(repo)` (локально, без сети) ПЕРВЫМ → выключенный флаг = нулевой сетевой
|
||
оверхед; `is_bug_task` делегирует в `labels.has_label` (ORCH-089-аппарат, источник истины — Plane
|
||
API, не payload). Чтение метки — только в `start_pipeline`, **никогда** в горячем `claim_next_job`
|
||
(NFR-4).
|
||
- **Хранение типа (D2):** аддитивная идемпотентная колонка `tasks.track TEXT DEFAULT 'full'`
|
||
(`_ensure_column`, паттерн `tasks.cancelled_at`); значения `'full'` (дефолт, ВСЕ существующие и
|
||
не-баг задачи) | `'bug'`. Хелперы `db.set_task_track`/`get_task_track` (отсутствие/NULL → `'full'`,
|
||
fail-safe). Читается в `advance_stage` из БД, не из сети.
|
||
- **Routing-override (D3):** врезка в `advance_stage` на ребре выхода из `analysis`: при `track='bug'`
|
||
(чистый предикат `bug_fast_track.skips_architecture`) `next_stage` → `development`, `next_agent` →
|
||
`developer` (минуя `architect`). `STAGE_TRANSITIONS`/`get_next_stage`/`get_agent_for_stage` — чистые,
|
||
1:1. Стамп `mark_brd_review_ended` расширен на `analysis → development` (честная метрика ORCH-087).
|
||
- **Эскалация (D5):** `POST /bug-fast-track/escalate?work_item=<id>` сбрасывает `track` `'bug'→'full'`
|
||
→ следующий переход уходит в `architecture` (полный цикл). Плюс self-escalate мини-аналитика
|
||
(«баг сложный → полный пакет + `escalate: full-cycle`»).
|
||
- **Флаги** (`config.py`): `bug_fast_track_enabled` (kill-switch, env `ORCH_BUG_FAST_TRACK_ENABLED`),
|
||
`bug_fast_track_label` (дефолт `Bug`), `bug_fast_track_repos` (CSV; **пусто → self-hosting only**).
|
||
`False`/неприменимый репо → старт и маршрут байт-в-байт прежние (нулевая регрессия для enduro и
|
||
orchestrator). Наблюдаемость — read-only блок `bug_fast_track` в `GET /queue` (флаг/метка/область +
|
||
счётчик багфикс-задач + метрика пропущенных стадий `architecture`) + отметка `🐞` в Telegram-карточке
|
||
(never-raise). Композиция: багфикс-задача — обычная задача репо для serial-gate (ORCH-088, не
|
||
обходит его); `autoApprove`/`autoDeploy` (ORCH-089), coverage-gate (ORCH-027, союзник BR-4),
|
||
merge-gate (ORCH-043) — штатно. **Инфра-предусловие:** создать метку **`Bug`** в Plane-проекте ORCH
|
||
(её отсутствие = fail-safe полный цикл). Детали —
|
||
`docs/work-items/ORCH-019/06-adr/ADR-001-bug-fast-track.md`,
|
||
`docs/architecture/adr/adr-0032-bug-fast-track.md`.
|
||
|
||
## Гейт покрытия тестами (ORCH-027)
|
||
Существующие тестовые гейты (`check_ci_green`, `check_tests_passed`, merge-gate re-test) судят
|
||
только по **факту** прохождения, не по **полноте** — ни один не замечает «300 строк кода, 0
|
||
тестов», и при пакетном автономном прогоне (ORCH-088) покрытие монотонно деградирует. Введён
|
||
**детерминированный (без LLM) под-гейт ребра `deploy-staging → deploy`** по образцу security-гейта
|
||
(ORCH-022): leaf `src/coverage_gate.py` (never-raise) + тонкая обёртка `check_coverage_gate` в
|
||
`QG_CHECKS` + врезка `_handle_coverage_gate` в `advance_stage`. **Инвариант:** `STAGE_TRANSITIONS` /
|
||
семантика существующих `check_*` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/
|
||
`staging_status:`/`security_status:`) — байт-в-байт прежние; новая БД-таблица аддитивна (NFR-5).
|
||
- **Точка/порядок:** **ПОСЛЕ merge-gate** (покрытие меряется на догнанном `auto_rebase_onto_main`
|
||
HEAD — ровно том коде, что landed в `main`) и **ДО image-freshness** (фейл до дорогого
|
||
docker-rebuild). Порядок под-гейтов: **security → merge → coverage → image-freshness.** FAIL →
|
||
штатный откат на `development` (+ инкремент developer-retry, cap `MAX_DEVELOPER_RETRIES`) **и
|
||
освобождение merge-lease** (merge-gate держал его на своём PASS — зеркало image-freshness rollback).
|
||
- **Измерение:** `python -m pytest tests/ --cov=src --cov-report=json` в изолированном per-branch
|
||
worktree (`ensure_worktree`); метрика — `totals.percent_covered` (line coverage `src/`). Измеритель
|
||
за `measure_coverage(repo, branch) -> float | None` (стек-расширяемость BR-6). Тайм-аут
|
||
`coverage_run_timeout_s`. Новая pip-зависимость `pytest-cov`.
|
||
- **Решение — чистая функция** `compute_coverage_verdict(measured, baseline, floor, policy, epsilon)
|
||
-> (ok, reason)`: `absolute` → `measured ≥ floor−ε`; `baseline` → `measured ≥ baseline−ε`; `both`
|
||
(дефолт) → оба; `baseline is None` (bootstrap) → baseline-условие не применяется. `epsilon` —
|
||
допуск на шум измерения (анти-флап у границы).
|
||
- **Базовая линия — аддитивная БД-таблица** `coverage_baseline(repo PK, coverage, source_sha,
|
||
updated_at)` (`CREATE TABLE IF NOT EXISTS`; хелперы `db.get_coverage_baseline`/
|
||
`ratchet_coverage_baseline`/`set_coverage_baseline`). Наращивание **только вверх** в choke-point
|
||
подтверждённого merge `_handle_merge_verify` (ребро `deploy → done`): `ratchet_baseline_on_merge`
|
||
читает измеренное из `18-coverage-report.md` (single source of truth), атомарный compare-and-set
|
||
`UPDATE … WHERE coverage <= measured` под держимым merge-lease (ORCH-043) → базовая линия не падает
|
||
даже при гонке; bootstrap засевается первым применимым merge.
|
||
- **Условность (как ORCH-22/43/58):** `coverage_gate_enabled` (kill-switch; `False` → 1:1 как до
|
||
ORCH-027) + `coverage_gate_repos` (CSV; **пусто → self-hosting only** `is_self_hosting_repo` →
|
||
enduro не затронут, no-op `(True, "N/A")`); `applies(repo)` (локально) ПЕРВЫМ — дорогой прогон
|
||
только при `applies==True`. Ошибка инструмента/непарсимая метрика → **fail-open + WARNING** по
|
||
умолчанию (`coverage_tool_fail_closed=False`, анти-петля); флаг → fail-closed.
|
||
- **Артефакт `18-coverage-report.md`** (frontmatter `coverage_status: PASS|FAIL` +
|
||
`measured_coverage`/`baseline`/`floor`/`policy`/`epsilon`/`delta`), вердикт читается ТОЛЬКО из
|
||
frontmatter через `src/frontmatter.py` (single source of truth, как `security_status:`).
|
||
Наблюдаемость — read-only блок `coverage` в `GET /queue`; при FAIL — `send_telegram` с кликабельным
|
||
номером, измеренным/порогом/дельтой; опциональный ручной override `POST /coverage/baseline`.
|
||
Флаги `ORCH_COVERAGE_*` (`MIN_PERCENT`/`POLICY`/`EPSILON`/`TOOL_FAIL_CLOSED`/`RUN_TIMEOUT_S`).
|
||
Self-hosting-безопасно: гейт только мерит/читает/пишет/решает — не деплоит/не рестартит прод/не
|
||
пушит `main`. **Инфра-предусловие:** `pytest-cov` в прод/staging-образе. Детали —
|
||
`docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md`,
|
||
`docs/architecture/adr/adr-0029-coverage-gate.md`.
|
||
|
||
## Конвенции
|
||
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
||
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
|
||
- ADR per work-item: `docs/work-items/<plane-id>/06-adr/ADR-NNN-slug.md`
|
||
- Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md`
|
||
- Work items: `docs/work-items/<plane-id>/`
|
||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза. **ORCH-52c (ORCH-076):** парсинг frontmatter сведён к единому контракту `src/frontmatter.py` (reader `read_frontmatter_value` — BC; единый парс-примитив `parse_frontmatter`; writer `render/write_frontmatter`; валидатор схемы `validate_schema`/`REQUIRED_FIELDS` — warning-only по умолчанию, hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`). Пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) читают через ОДНУ точку парсинга; семантика вердиктов и `STAGE_TRANSITIONS`/состав `QG_CHECKS` — 1:1. Формальная спека «стадия → обязательный выход» + обязательная frontmatter-схема — `docs/_standards/HANDOFF_PROTOCOL.md`
|
||
|
||
## Артефакты задачи (`docs/work-items/<plane-id>/`)
|
||
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`, `16-post-deploy-log.md` (post-deploy наблюдение, ORCH-021), `17-security-report.md` (security-гейт: `security_status:`/secrets/deps, ORCH-022), `18-coverage-report.md` (coverage-гейт: `coverage_status:`/measured/baseline, ORCH-027).
|
||
|
||
**Стандарт документов (ORCH-075, ORCH-52b):** структура каждого дока, карта «стадия→агент→документ→гейт→machine-key» и конвенция ADR-naming зафиксированы в `docs/_standards/PIPELINE_DOCS.md` (golden source); копируемые скелеты — в `docs/_templates/`. Перед написанием номерного дока бери скелет из `docs/_templates/` и не меняй имя machine-key frontmatter (регистр чувствителен — иначе гейт упадёт ложно).
|
||
|
||
## Правила для агентов
|
||
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
|
||
2. **Документация = golden source наравне с кодом.** Изменил функционал → обнови доку В ТОМ ЖЕ PR. Архитектурное решение → заведи ADR (формат — `docs/_standards/PIPELINE_DOCS.md` §4). Структура номерных доков и шаблоны — `docs/_standards/PIPELINE_DOCS.md` + `docs/_templates/`. Обнови `CHANGELOG.md`.
|
||
3. Никогда не править артефакты других этапов.
|
||
4. Никогда не комментировать ТЗ задним числом — если ТЗ не годится, возвращай в Анализ.
|
||
5. Никогда не закрывать задачу самостоятельно — это делает CI / финальная стадия.
|
||
6. **Reviewer проверяет: обновлена ли документация. Нет → REQUEST_CHANGES.** Это включает **обзорные доки** (ORCH-079, 52f — финал эпика 52): если PR закрывает пункт `README.md` «Известные ограничения», но README не обновлён → finding ≥P1 (витрина проекта не должна выдавать решённое за открытое).
|
||
7. Не использовать `--no-verify` без явного одобрения Owner.
|
||
8. Секреты — только в `.env`/`.env.staging` на хосте, в гит НЕ коммитятся (канон — `.env.example`).
|
||
9. **Трассировка маркеров (ORCH-078, ORCH-52e):** правишь строку/блок с маркером `ORCH-NNN` →
|
||
ПЕРЕД изменением прочитай его `docs/work-items/ORCH-NNN/06-adr/` и не сломай зафиксированный
|
||
инвариант; блок с 3+ маркерами → опирайся на сводный сквозной ADR. Стандарт маркеров (формат,
|
||
размещение, fallback-доступ, анти-археология, каноничное правило чтения) — `docs/_standards/TRACEABILITY.md`.
|
||
|
||
## ⚠️ Self-hosting — оркестратор правит САМ СЕБЯ
|
||
Задачи проекта ORCH меняют инструмент, который СЕЙЧАС работает в продакшене и обслуживает ДРУГИЕ проекты (enduro-trails) из ОДНОГО инстанса с ОБЩЕЙ БД и общей очередью.
|
||
- **НЕ перезапускать / не ронять прод-контейнер** `orchestrator` в рамках задачи — встанет конвейер всех проектов.
|
||
- Любой деплой/рестарт self = групповой риск. Детали и топология — `docs/operations/INFRA.md`.
|
||
- Стадия `deploy-staging` (порт 8501) — обязательная страховка перед прод-деплоем орка.
|
||
- Прод-деплой орка запускается ТОЛЬКО переводом задачи на стадии `deploy` в выделенный
|
||
Plane-статус **«Confirm Deploy»** (ORCH-059). Статус `Approved` — человеческий гейт
|
||
конвейера и прод-деплой НЕ запускает (на `deploy` — no-op). Это разделяет «одобрить
|
||
артефакт» и «выкатить в прод», чтобы привычный approve не ронял прод случайным кликом.
|
||
|
||
---
|
||
*Паспорт проекта orchestrator. Поддерживается агентами при каждой доработке. Изолирован: описывает только этот проект (канон per-repo, см. ORCH-9).*
|