Compare commits

...

8 Commits

Author SHA1 Message Date
311121e6f8 deployer(ET): auto-commit from deployer run_id=510
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Successful in 37s
2026-06-09 22:08:42 +03:00
37a28684f4 tester(ET): auto-commit from tester run_id=509
All checks were successful
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 33s
2026-06-09 22:05:05 +03:00
20a3ab2444 reviewer(ET): auto-commit from reviewer run_id=508
All checks were successful
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 37s
2026-06-09 22:02:59 +03:00
95345c3c77 fix(notifications): tracker card — status-map completeness, rollback reflection, stage-metric summation (ORCH-091)
All checks were successful
CI / test (push) Successful in 32s
CI / test (pull_request) Successful in 33s
Three verified live-card defects in src/notifications.py (ORCH-067/087),
all additive and indication-only (STAGE_TRANSITIONS / QG_CHECKS / check_* /
transport / DB schema untouched; never-raise; revert = git revert):

- Деф.1 (D1): _STAGE_STATUS_LABEL covered 8 of 10 STAGE_TRANSITIONS keys —
  deploy-staging and cancelled (ORCH-090) fell back to the misleading
  "To Analyse". Added deploy-staging→"Deploying (staging)",
  cancelled→"Cancelled"; replaced the runtime fallback for an UNMAPPED stage
  with a neutral capitalized label (_neutral_stage_label). created stays an
  explicit "To Analyse"; broken/None input degrades safely. Map completeness
  is asserted programmatically from STAGE_TRANSITIONS.keys() (single source of
  truth), not a static list.
- Деф.2 (D2): the stage-row loop drew  for any stage with a finished agent
  run regardless of position — after a rollback the card showed the absurd
  " Внедрение + 🔄 Разработка". Added read-only _pipeline_pos from the
  STAGE_TRANSITIONS order and a suppression gate ( only when
  current_pos >= _pipeline_pos(stage_key)); deploy-staging→deploy normalization
  applied ONLY to the current position; is_active_stage untouched.
- Деф.3 (D3): _stage_line took only the LAST run (ORCH-069: developer 3 runs
  Σ $3.98 rendered ~$0.00). It now aggregates ALL of the agent's runs with the
  same per-run formulas as the task totals → strict convergence with
  SUM(agent_runs) by task_id; model/effort/attempt come from the last run.

Tests: test_tracker_status_line.py (ORCH-091 TC-01..TC-03 + updated tc06);
new test_tracker_rollback_metrics.py (TC-05..TC-08). Full suite green (1370).
Docs: CHANGELOG + internals.md (architecture README already updated by architect).

Refs: ORCH-091
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:58:26 +03:00
5ccb3c8c1a architect(ET): auto-commit from architect run_id=506
All checks were successful
CI / test (push) Successful in 31s
2026-06-09 21:48:55 +03:00
74440ccac1 architect(ET): auto-commit from architect run_id=505
All checks were successful
CI / test (push) Successful in 32s
2026-06-09 21:46:55 +03:00
f4c147943a analyst(ET): auto-commit from analyst run_id=504
All checks were successful
CI / test (push) Successful in 34s
2026-06-09 21:40:07 +03:00
2b74363a5a docs: init ORCH-091 business request
All checks were successful
CI / test (push) Successful in 31s
2026-06-09 21:36:14 +03:00
17 changed files with 1360 additions and 23 deletions

View File

@@ -1,4 +1,4 @@
Work item: ORCH-088
Work item: ORCH-091
Repo: orchestrator
Branch: feature/ORCH-088-orch-88-10-20
Branch: feature/ORCH-091-bug-to-analyse-stage-deploy-st
Stage: development

View File

@@ -3,6 +3,12 @@
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
## [Unreleased]
- **Live-карточка трекера: полнота карты статусов, отражение откатов, суммирование метрик стадии по попыткам** (ORCH-091, `fix`): три верифицированных дефекта рендера Telegram-карточки (`src/notifications.py`, ORCH-067/087). **Аддитивно, never-raise, без нового поведения конвейера:** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / транспорт нотификаций / схема БД — **не тронуты** (затронут ровно один модуль индикативного слоя); kill-switch не требуется (рендер деградирует безопасно, откат = `git revert`).
- **Деф.1 — застрявший заголовок «To Analyse» (FR-1/2/3, AC-1/2/3):** `_STAGE_STATUS_LABEL` покрывал 8 из 10 ключей `STAGE_TRANSITIONS``deploy-staging` и `cancelled` (ORCH-090) выпадали в дефолт-«To Analyse» (ложный «первый статус» на стадии staging-деплоя). Карта расширена: `deploy-staging → "Deploying (staging)"` (plain-стиль активной стадии, суффикс «(staging)» снимает коллизию с prod-overlay `_LIVE_BRANCH_LABELS['deploying']` и с pause-лейблом `deploy`), `cancelled → "Cancelled"` (offline-база ORCH-090, совпадает с overlay-лейблом → нет конфликта precedence). Runtime-фолбэк `plane_status_label` для **немаппленной** (будущей/неизвестной) стадии заменён с «To Analyse» на **нейтральный** капитализированный лейбл (`_neutral_stage_label`, `"deploy-staging" → "Deploy Staging"`); `created` остаётся явным ключом → честная «To Analyse»; битый/None-вход → безопасный дефолт. Полнота карты гарантируется **программно** тестом, итерирующим `STAGE_TRANSITIONS.keys()` (единый источник истины) — новая стадия без курируемого лейбла даёт красный тест; автогенерация лейблов в самом модуле запрещена (карта остаётся курируемой/человекочитаемой).
- **Деф.2 — ложная картина при откате (FR-4, AC-4):** цикл рендера выводил `✅`-строку для каждой стадии с завершённым прогоном её агента **без учёта позиции** относительно текущей — после отката (`deploy-staging → development` ORCH-043, `review → development` REQUEST_CHANGES) карточка показывала абсурд «✅ Внедрение … + 🔄 Разработка». Введён лёгкий read-only хелпер `_pipeline_pos` от **порядка `STAGE_TRANSITIONS`** (не от `_TRACKER_STAGES`, который не содержит `deploy-staging`/`cancelled` и не авторитетен по порядку); гейт подавления: `✅`-строка рисуется только если `current_pos >= _pipeline_pos(stage_key)`. Нормализация `deploy-staging → deploy` применяется **только** к вычислению текущей позиции (схлопнутая строка «Внедрение» несёт `stage_key="deploy"`); `is_active_stage`**без изменений** (нулевой регресс активного рендера). Подавлённые откатом прогоны по-прежнему входят в тоталы задачи (намеренная семантика отката).
- **Деф.3 — занижение метрик строки стадии (FR-5, AC-5):** `_stage_line` брал ПОСЛЕДНИЙ прогон (`last_done`), теряя предыдущие попытки (верифицировано на ORCH-069: developer 3 прогона Σ $3.98 → карточка показывала ~$0.00). Теперь `_stage_line` агрегирует **ВСЕ** `agent_runs` агента стадии теми же per-run-формулами, что и блок тоталов (`Σ cost_usd`, `Σ _input_total`, `Σ output_tokens`, `Σ _duration_seconds`); модель/эффорт/«попытка N» берутся из последнего прогона (`id ASC`). Каждый агент привязан ровно к одной строке `_TRACKER_STAGES` → строгий инвариант сходимости: Σ(строк стадий) ≡ тоталы задачи ≡ `SUM(agent_runs)` по `task_id`. Формат строк/тоталов и эффорт-суффикс (ORCH-087) — байт-в-байт.
- **Совместимость/регресс (NFR-2, AC-6):** In Review (brd-clock), Awaiting Deploy (`deploy`), Done, live-overlay ветки (Needs Input / Blocked / Rejected / Cancelled / Confirm Deploy / Deploying / Monitoring), строка «Подтверждение BRD», формат строк/тоталов, эффорт-суффикс — без изменений; все существующие тесты карточки зелёные. Перед правкой кода, помеченного ORCH-067/087/090, прочитаны их ADR — инварианты (single-card, never-raise, разделение offline-ядра и live-overlay, терминал `cancelled`) сохранены.
- Тесты: `tests/test_tracker_status_line.py` (ORCH-091 TC-01..TC-03: полнота карты от `STAGE_TRANSITIONS`, staging-лейбл, нейтральный фолбэк/never-raise; обновлён `test_tc06_*` под нейтральный фолбэк), новый `tests/test_tracker_rollback_metrics.py` (TC-05..TC-08: подавление `✅` при откате + анти-регресс forward-progress/`deploy-staging`-строка; суммирование метрик developer 3 прогона ≈ $3.98; сходимость тоталов с `SUM(agent_runs)`; never-raise на NULL-таймстампах/битой стадии). Полный регресс `tests/ -q` зелёный (1370). ADR: `docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md`. Откат: `git revert` (docs/code-only, один модуль, без миграций/kill-switch).
- **Отмена задачи: Plane-статус STOP (остановка агента + полный сброс) + закрытие дыры релонча** (ORCH-090, `feat`): выделенный Plane-статус **STOP** — единый декларативный механизм отмены задачи вместо ручной хирургии по БД/процессам. Вводит **новое системное терминальное состояние `cancelled`** (стадия `tasks.stage='cancelled'` + job-исход `jobs.status='cancelled'`), равноправное `done`. **Аддитивно, под kill-switch, never-raise, restart-safe:** `STAGE_TRANSITIONS` (exit-гейты рёбер) / `QG_CHECKS` / `check_*` / семантика существующих статусов — **не тронуты** (`cancelled` — терминальный сток, не новое ребро); enduro не затронут; при `stop_status_enabled=false` — нулевая регрессия.
- **Распознавание (fail-closed):** новый логический ключ `stop` в `_PLANE_NAME_TO_KEY` (`"STOP" → "stop"`), **намеренно отсутствует** в `_DEFAULT_STATES` (по образцу `confirm_deploy`/ORCH-059) → доска без статуса STOP резолвит `None` → ветка не активируется (нет `KeyError`, нет слепой отмены). `handle_issue_updated` маршрутизирует `stop``handle_stop``stage_engine.cancel_task` (проверяется ПЕРВЫМ, до to_analyse/approved/rejected).
- **Полный сброс (вне критичного окна, AC-1..AC-4):** graceful SIGTERM активного агента через переиспользуемый каскад `launcher.stop_process` (вынесен из `_watchdog`: SIGTERM → grace → SIGKILL) по `jobs.pid`; `db.cancel_jobs_for_task` (queued/running → терминальный `cancelled`, нигде не реквью'ится — `claim_next_job` берёт только `queued`); `git_worktree.remove_worktree` + новый never-raise `src/gitea.py::delete_remote_branch` (удаляет **только** feature-ветку; `main`/`master` — явный гард-отказ; без force-push); durable `stage='cancelled'` + `cancelled_at`; **тумбстон** натуральных ключей суффиксом `#cancelled-<id>`. Docs-артефакты (`01..17`) сохраняются.

View File

@@ -15,7 +15,7 @@
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
- **Disk-watchdog** (`src/disk_watchdog.py`, ORCH-063 — [adr-0024](adr/adr-0024-disk-watchdog.md)) — фоновый daemon-поток (каркас `reconciler`/`job_reaper`), стартует/останавливается в `main.lifespan` (старт последним — после `reaper.start()`; стоп первым в reverse-порядке; гард `disk_monitor_enabled`). Каждые `disk_monitor_interval_s` (дефолт 300с) меряет заполнение **хост-ФС** по смонтированным bind-путям (`/repos`, `/app/data`) через stdlib `shutil.disk_usage` (не overlay `/` контейнера, не субпроцесс `df`; дедуп путей по `st_dev`). Решение об алерте — pure-функция `decide_action(used_pct, threshold, prev_state, now, realert_s)`: алерт на пересечении порога (дефолт **85%**), cooldown-повтор `disk_monitor_realert_s` (анти-спам, не на каждом тике), однократный recovery при возврате ниже порога. Алерт — `send_telegram` (notifying, best-effort). Состояние анти-спама — in-memory (без миграции БД). never-raise (per-path/per-tick/per-send); только читает и уведомляет — не трогает диск/контейнер, не рестартит прод (self-hosting безопасность). Kill-switch `ORCH_DISK_MONITOR_ENABLED`; снимок — блок `disk_monitor` в `GET /queue` (`enabled`/`threshold_pct`/`interval_s`/`realert_s`/`paths`[`used_pct`/`free_gb`/`alerting`/`last_alert_at`]). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. Детали — `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`.
- **Build-cache-pruner** (`src/build_cache_pruner.py`, ORCH-062 — [adr-0025](adr/adr-0025-build-cache-pruner.md)) — фоновый daemon-поток (каркас `disk_watchdog`), стартует/останавливается в `main.lifespan` (старт последним — после `disk_watchdog.start()`; стоп первым в reverse; гард `build_cache_prune_enabled`). «Вторая половина» disk-watchdog: **watchdog сигналит — pruner убирает**. Каждые `build_cache_prune_interval_s` (дефолт 21600с = 6ч) выполняет **строго `docker builder prune -f --filter until=<until>`** (BuildKit GC; дефолт `until=24h` — удаляет build cache старше суток, тёплый кэш сохраняет; `-a` опционально, только в паре с фильтром). Затрагивает **только** build cache — НЕ образы/контейнеры; рестарт docker daemon/прода не выполняется (self-hosting безопасность). В контейнере нет `docker` CLI (`Dockerfile:11`), поэтому уборка идёт **на хосте через ssh** каналом `deploy_ssh_user@deploy_ssh_host` (как `image_freshness`/`self_deploy`); пустой `deploy_ssh_host` → тик no-op (скоуп на self-host). never-raise (per-команда/per-tick); учёт результата in-memory (без миграции БД). Kill-switch `ORCH_BUILD_CACHE_PRUNE_ENABLED`; снимок — блок `build_cache_prune` в `GET /queue` (`enabled`/`interval_s`/`until`/`last_run_ts`/`last_reclaimed`/`last_error`). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. Детали — `docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md`.
- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7 и [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md).
- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. **ORCH-091 (индикация-only):** три корректности рендера — (1) `_STAGE_STATUS_LABEL` покрывает ВСЕ ключи `STAGE_TRANSITIONS` (добавлены `deploy-staging`→«Deploying (staging)», `cancelled`→«Cancelled»; полнота гарантируется тестом по `stages.STAGE_TRANSITIONS`, не статичным списком — NFR-3), runtime-фолбэк для неизвестной стадии стал нейтральным (капитализированное имя) вместо «To Analyse»; (2) при откате конвейера `✅`-строки стадий ПОЗЖЕ текущей позиции (позиция — из порядка `STAGE_TRANSITIONS`, с нормализацией `deploy-staging→deploy` только в гейте подавления; `is_active_stage` не тронут) больше не рисуются; (3) строка стадии суммирует ВСЕ `agent_runs` агента (Σ cost/токены/время теми же формулами, что блок тоталов) → строгая сходимость с `SUM(agent_runs)`. Только `src/notifications.py` + тесты; `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/транспорт — не тронуты. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7, [ADR-087](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md) и [ORCH-091 ADR-001](../work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md).
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость).

View File

@@ -134,8 +134,10 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
**Текст карточки (оба режима, ORCH-042):** метка `Подтверждение BRD` (была «Ревью БРД»); после прохождения approve-gate строка BRD начинается с ✅ (ветка ожидания сохраняет ⏸️/⏳); русские display-labels стадий (`Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение`); финальная строка `📦 Внедрено` (было `deployed`). Меняются только отображаемые строки — ключи стадий и имена агентов (завязаны на `_STAGE_ACTIVE_AGENT`, `last_done`, БД) не трогаются.
**Строки стадий: отражение откатов + суммирование метрик (ORCH-091).** Цикл рендера строк стадий (`render_task_tracker``_stage_line`) исправлен по двум осям. (1) **Откат (Деф.2):** `✅`-строка стадии рисуется только если её позиция в конвейере `≤` текущей позиции задачи; позиция берётся из порядка `STAGE_TRANSITIONS` (read-only хелпер `_pipeline_pos`, never-raise; неизвестная стадия → «далёкое будущее» → ✅ не пере-подавляется) с нормализацией `deploy-staging → deploy` ТОЛЬКО в гейте подавления (схлопнутая строка «Внедрение» несёт `stage_key="deploy"`). После отката (`deploy-staging → development`, `review → development`) строки стадий ПОЗЖЕ текущей больше не рисуются как пройденные — пропадает абсурд «✅ Внедрение + 🔄 Разработка»; `is_active_stage` не тронут. (2) **Метрики (Деф.3):** `_stage_line` агрегирует ВСЕ `agent_runs` агента стадии (Σ cost / Σ токены / Σ время теми же per-run-формулами, что блок тоталов задачи), а не последний прогон — каждый агент привязан ровно к одной строке `_TRACKER_STAGES`, поэтому Σ(строк стадий) ≡ тоталы ≡ `SUM(agent_runs)` по `task_id`; модель/эффорт/«попытка N» берутся из последнего прогона. Прогоны, подавлённые откатом, по-прежнему входят в тоталы (намеренная семантика отката).
**Строка Plane-статуса и кликабельный номер (ORCH-067, слой B — индикация).** Под заголовком карточка несёт строку `📍 <Plane-статус>` по модели ORCH-066. Источник — двухслойный, контракт **never raises**:
- **Оффлайн-ядро** `plane_status_label(task_row)` — чистая функция БЕЗ сети: `stage → статус` (`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`, `development→Development`, `review→Code-Review`, `testing→Testing`, `deploy→⏸️ Awaiting Deploy`, `done→Done`) + `⏸️ In Review` из brd-часов (`brd_review_started_at` задан, `…_ended_at` пуст). Неизвестная/битая стадия → безопасный дефолт `To Analyse`.
- **Оффлайн-ядро** `plane_status_label(task_row)` — чистая функция БЕЗ сети: `stage → статус` (`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`, `development→Development`, `review→Code-Review`, `testing→Testing`, `deploy-staging→Deploying (staging)` [ORCH-091], `deploy→⏸ Awaiting Deploy`, `done→Done`, `cancelled→Cancelled` [ORCH-091]) + `⏸️ In Review` из brd-часов (`brd_review_started_at` задан, `…_ended_at` пуст). **ORCH-091:** карта `_STAGE_STATUS_LABEL` покрывает ВСЕ ключи `STAGE_TRANSITIONS` (полнота — тестом, не статичным списком); неизвестная/будущая стадия → нейтральный фолбэк (капитализированное имя стадии), а НЕ «To Analyse» (он остаётся лишь явным лейблом `created` и безопасной деградацией на истинно-битом входе).
- **Live-overlay** `_live_plane_branch_override` — best-effort: дорисовывает ветви-статусы, неразличимые оффлайн (Needs Input / Blocked / Rejected / Cancelled / Deploying / Monitoring after Deploy), чтением живого Plane-статуса (`fetch_issue_state` с коротким `tracker_live_status_timeout_s`, TTL-кэш `tracker_live_status_ttl_s`, kill-switch `tracker_live_status`). Любой сбой / выключенный флаг / нехватка данных → оффлайн-метка; `⏸️ In Review` (авторитет brd-часов) overlay не консультирует. Анти-false-positive: `deploying/monitoring`, алиасящие базовый UUID на проекте без выделенного статуса (enduro), не вызывают override.
**Кликабельный номер задачи (ORCH-067).** Номер в заголовке карточки И во всех уведомлениях орка, где упоминается `work_item_id`, — HTML-ссылка на issue в Plane через общий `plane_issue_link` / `link_for` (URL строит `_plane_issue_url` с loopback/workspace/project-гардами, переиспользуя резолв ORCH-017). Fail-safe: при нехватке любого из (web-base/не-loopback, workspace, project_id, plane_issue_id) → `html.escape(work_item_id)` без `<a>`; динамические части экранируются, `<a>`-разметка валидна под `parse_mode=HTML`. Алерты `stage_engine`/`launcher`/`security_gate`/`reconciler` переведены на `link_for` (резолвит `repo`+`plane_issue_id` из БД по `task_id` или `work_item_id`).

View File

@@ -0,0 +1,7 @@
# Business Request: BUG: заголовок-строка карточки застревает на «To Analyse» на stage=deploy-staging (нет ключа в _STAGE_STATUS_LABEL)
Work Item ID: ORCH-091
## Description
TBD

View File

@@ -0,0 +1,137 @@
---
work_item: ORCH-091
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# 01 — BRD (бизнес-требования): ORCH-091 — Карточка трекера: фикс «To Analyse» на deploy-staging, отражение откатов, суммирование метрик по попыткам
Work Item: **ORCH-091** · Repo: **orchestrator** · Стадия: analysis
## 1. Бизнес-контекст и проблема
Live Telegram-карточка задачи (ORCH-067, единственная карточка на задачу, рендер
`src/notifications.py::render_task_tracker`) — основной канал, по которому Owner/Слава
наблюдают прогресс конвейера. Карточка обязана показывать **честную** текущую картину.
Объединены три верифицированных по коду и БД прода (09.06) дефекта одной карточки
(ORCH-072 закрыт как дубль; ORCH-091 расширена до полного объёма):
- **Дефект 1 (косметика, но вводит в заблуждение).** Заголовок-строка статуса карточки
(`📍 <status_label>`) застревает на «To Analyse», когда задача реально на стадии
`deploy-staging`. Корень верифицирован: словарь `_STAGE_STATUS_LABEL`
(`src/notifications.py` ~стр. 940) содержит 8 ключей (`created/analysis/architecture/
development/review/testing/deploy/done`), а реальные значения `tasks.stage` — это ключи
`STAGE_TRANSITIONS` (`src/stages.py`), среди которых есть `deploy-staging`. Ровно эта
стадия не покрыта → `.get(stage, _DEFAULT_STATUS_LABEL)` отдаёт дефолт «To Analyse»
(`_DEFAULT_STATUS_LABEL`, ~стр. 950). Программно проверено: из 9 реальных стадий не
покрыта **ровно одна**`deploy-staging` (предпоследняя перед прод-деплоем, видна
чаще всего). Сам дефолт-«To Analyse» — мина на будущее: любая новая стадия даст ложный
«первый статус».
- **Дефект 2 (ложная картина при откате).** При rollback по конвейеру (напр. merge-gate
`deploy-staging → development`, ORCH-43; или REQUEST_CHANGES `review → development`)
верхние строки `✅ пройдено` (Код-ревью / Тестирование / Внедрение) НЕ снимаются, а внизу
снова `🔄 Разработка`. Абсурд: «Внедрение готово ✅, но идёт Разработка 🔄». Корень:
цикл рендера в `render_task_tracker` (~стр. 474505) выводит `✅`-строку для каждой
стадии `_TRACKER_STAGES`, у чьего агента есть завершённый прогон (`last_done`), без
учёта позиции стадии относительно текущей.
- **Дефект 3 (реальное занижение тоталов, не косметика).** Строка стадии берёт ПОСЛЕДНИЙ
прогон агента (`run = last_done.get(agent)`, ~стр. 475; `_stage_line`), теряя предыдущие
попытки. На задаче с ретраями метрики стадии занижены. Верифицировано на ORCH-069
(`task_id=54`, прод 09.06): developer = 3 прогона Σ $3.98 (карточка показывала ~$0.00 за
«Разработка»), reviewer = 3 Σ $2.10, tester = 2 Σ $1.03, deployer = 2 Σ $1.59. Источник
истины — таблица `agent_runs` (`cost_usd`, `input_tokens`, `output_tokens`,
`cache_read_tokens`, `cache_creation_tokens`, `started_at`/`finished_at`).
> **Замечание (факт кода, не противоречие).** Блок тоталов задачи (`💰`/`🔢`/`⏱ Агенты`)
> в текущем worktree уже суммирует ВСЕ прогоны (`render_task_tracker` ~стр. 388404).
> Заниженной остаётся **строка стадии** (`_stage_line` показывает только последний прогон).
> Требование G4/AC-5 формулируется на уровне строки стадии и инварианта сходимости тоталов
> с `SUM(agent_runs)` — реализация/архитектура подбора агрегата за архитектором.
## 2. Объём (scope)
### В объёме
- Покрытие `_STAGE_STATUS_LABEL` всеми ключами `STAGE_TRANSITIONS` из единого
программного источника истины (не «на глаз», не дублирующим списком).
- Осмысленный staging-лейбл для `deploy-staging`, согласованный с моделью статусов
ORCH-066/059.
- Нейтральный фолбэк для истинно неизвестной/битой стадии (вместо «To Analyse»).
- Отражение откатов: снятие `✅` со стадий ПОСЛЕ текущей позиции задачи.
- Метрика строки стадии = Σ всех `agent_runs` стадии (💰 стоимость / 🔢 токены / ⏱ время),
с сохранением сходимости тоталов задачи с `SUM(agent_runs)` по `task_id`.
- Тесты на полноту карты стадий, суммирование метрик, отражение отката; `CHANGELOG.md`.
### Вне объёма
- Изменение `STAGE_TRANSITIONS`, схемы БД, реестра `QG_CHECKS`/`check_*`, транспорта
нотификаций (`send/edit/delete_telegram`).
- Live-overlay ветки (Needs Input / Blocked / Rejected / Cancelled / Confirm Deploy /
Deploying / Monitoring) — работают, не трогаем.
- Архитектурное решение «как реализовать» (ordering-источник, форма агрегата) —
зона архитектора (`06-adr/`).
## 3. Заинтересованные стороны
- **Заказчик / приёмка:** Owner (homenet542), Слава (нашёл дефекты 08.06).
- **Затрагивается:** все наблюдатели карточек конвейера всех проектов (общий прод-инстанс,
self-hosting). Косметика карточки — для всех репо (orchestrator + enduro-trails).
## 4. Бизнес-требования (BR)
- **BR-1 (Деф.1, G1)** — `_STAGE_STATUS_LABEL` покрывает КАЖДЫЙ ключ `STAGE_TRANSITIONS`;
полнота гарантируется программно (итерация по единому источнику истины `src/stages.py`),
а не статичным списком. Для каждой реальной стадии `plane_status_label` возвращает
непустой осмысленный лейбл (не дефолт-«To Analyse», кроме реального `created`).
- **BR-2 (Деф.1, G1)** — `stage='deploy-staging'` → осмысленный staging-лейбл (напр.
«Deploying (staging)» / «⏳ Staging»), согласованный с моделью статусов ORCH-066/059.
- **BR-3 (Деф.1, G2)** — фолбэк для истинно неизвестной/битой стадии — нейтральный (напр.
«В работе» / stage capitalized), НЕ «To Analyse», чтобы будущая стадия не давала ложный
«первый статус». `plane_status_label` остаётся never-raise.
- **BR-4 (Деф.2, G3)** — при откате стадии карточка отражает ФАКТИЧЕСКУЮ текущую позицию:
с стадий ПОСЛЕ точки отката снимается `✅`; текущая стадия отрисовывается как активная
(`🔄`). Сценарий-эталон: после `deploy-staging → development` Разработка = `🔄`,
Тестирование/Внедрение — НЕ `✅`.
- **BR-5 (Деф.3, G4)** — метрика строки стадии = СУММА всех `agent_runs` этой стадии
(по `task_id` + агент стадии) по трём метрикам: 💰 `Σ cost_usd`, 🔢 `Σ (input + output +
cache_read + cache_creation)`, ⏱ `Σ (finished_at started_at)`. Тоталы задачи = суммы по
всем стадиям и попыткам, сходятся с `SUM(agent_runs)` по `task_id`.
## 5. Нефункциональные требования (NFR)
- **NFR-1 (надёжность)** — `render_task_tracker` и `plane_status_label` остаются
**stateless / never-raise**: любая ошибка деградирует к безопасному выводу, конвейер
никогда не блокируется рендером карточки.
- **NFR-2 (совместимость / регресс)** — существующие метки и строки НЕ меняются: In Review
(brd-clock), Awaiting Deploy (`deploy`), Done, live-overlay ветки, строка `Подтверждение
BRD`, формат строк стадий/тоталов, эффорт-суффикс (ORCH-087). Изменение аддитивно.
- **NFR-3 (источник истины)** — полнота карты стадий выводится из `STAGE_TRANSITIONS`
программно; запрещено дублировать перечень стадий руками (анти-рассинхрон на будущее).
- **NFR-4 (self-hosting)** — изменения только в `src/notifications.py` + тесты + доки; без
правки `STAGE_TRANSITIONS`/схемы БД/QG; без рестарта прод-контейнера в рамках задачи.
## 6. Допущения и ограничения
- `tasks.stage` принимает строго значения-ключи `STAGE_TRANSITIONS` (включая
`deploy-staging`, `cancelled`). Это инвариант движка стадий.
- `cancelled` (ORCH-090) — системный терминал; его статус-лейбл уже рисуется live-overlay
(`_LIVE_BRANCH_LABELS['cancelled']`). Для offline-фолбэка `plane_status_label` он не
должен давать «To Analyse» (покрывается BR-3 нейтральным фолбэком; явный лейбл для
`cancelled` — на усмотрение архитектора, без конфликта с overlay).
- Источник метрик — `agent_runs`; стадия `deploy-staging` и `deploy` обслуживаются одним
агентом `deployer` — агрегат по агенту корректно покрывает обе (вопрос разнесения
staging/prod-прогонов по строкам — зона архитектора, не требование BRD).
- Telegram-ограничение 48ч на удаление сирот (ORCH-087) — вне объёма.
## 7. Критерии успеха
Карточка показывает корректный статус-заголовок на всех стадиях (включая `deploy-staging`),
не «лжёт» о пройденных стадиях после отката, и метрики строки стадии + тоталы сходятся с
`SUM(agent_runs)` по `task_id`. Полный регресс `pytest tests/ -q` зелёный. Детальные
PASS/FAIL — `03-acceptance-criteria.md`.
## 8. Риски
- Рассинхрон карты стадий с `STAGE_TRANSITIONS` в будущем (митигируется NFR-3 + тест полноты).
- Регресс существующих меток/строк при правке цикла рендера (митигируется NFR-2 + тесты).
- Неверная точка отсчёта «позиции» стадии для отката (ordering) → неверное снятие `✅`.
Детали — `10-tech-risks.md` (заполняет архитектор).

View File

@@ -0,0 +1,112 @@
---
work_item: ORCH-091
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# 02 — ТЗ (TRZ): ORCH-091 — Карточка трекера: полнота карты статусов, отражение откатов, суммирование метрик по попыткам
Work Item: **ORCH-091** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
> Архитектурное обоснование/решения (выбор ordering-источника для отката, форма агрегата
> метрик, явный лейбл для `cancelled`) — задача архитектора (`06-adr/`).
## 1. Сводка изменения
Три точечные правки в `src/notifications.py` (рендер live-карточки ORCH-067), все аддитивные,
без изменения транспорта, схемы БД, `STAGE_TRANSITIONS` и `QG_CHECKS`:
1. **Полнота карты статусов (Деф.1).** `_STAGE_STATUS_LABEL` должен покрывать все ключи
`STAGE_TRANSITIONS` (источник истины — `src/stages.py`), добавить `deploy-staging`
осмысленный staging-лейбл; нейтральный фолбэк вместо «To Analyse» для неизвестной стадии.
2. **Отражение откатов (Деф.2).** Цикл рендера строк стадий перестаёт показывать `✅` для
стадий, расположенных ПОСЛЕ текущей позиции задачи в конвейере.
3. **Суммирование метрик стадии (Деф.3).** Строка стадии агрегирует ВСЕ `agent_runs` агента
стадии (Σ стоимость/токены/время) вместо последнего прогона; тоталы сходятся с `SUM`.
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `src/notifications.py` | изменить: `_STAGE_STATUS_LABEL` (~940), `_DEFAULT_STATUS_LABEL` (~950), `plane_status_label` (~990), `render_task_tracker` (рендер строк стадий ~474505, агрегат метрик ~388404 / `_stage_line` ~445466) |
| `src/stages.py` | **только чтение** — импорт ключей `STAGE_TRANSITIONS` как источника истины для полноты карты (НЕ изменять) |
| `tests/test_tracker_status_line.py` | изменить/дополнить: полнота карты, staging-лейбл, нейтральный фолбэк |
| `tests/test_telegram_tracker.py` (или новый `tests/test_tracker_rollback_metrics.py`) | дополнить/создать: откат + суммирование метрик |
| `CHANGELOG.md` | изменить: запись ORCH-091 |
## 3. Функциональные требования
### FR-1 — Полнота `_STAGE_STATUS_LABEL` по `STAGE_TRANSITIONS` (BR-1)
- Для каждого ключа `STAGE_TRANSITIONS` (`created, analysis, architecture, development,
review, testing, deploy-staging, deploy, done, cancelled`) `plane_status_label` возвращает
непустой осмысленный лейбл.
- Полнота гарантируется **программно** от единого источника `src/stages.py::STAGE_TRANSITIONS`
(итерация/проверка пересечения ключей), а не статичным дублирующим списком (NFR-3).
- Сохранить спецветки `plane_status_label`: `analysis` + открытый brd-clock → `_IN_REVIEW_LABEL`
(без изменений).
### FR-2 — Staging-лейбл для `deploy-staging` (BR-2)
- `stage='deploy-staging'` → осмысленный лейбл (предлагается «Deploying (staging)» или
«⏳ Staging»; финальный текст согласует архитектор с моделью статусов ORCH-066/059).
- НЕ равен «To Analyse» и НЕ равен лейблу `deploy` (`⏸️ Awaiting Deploy …`).
### FR-3 — Нейтральный фолбэк (BR-3)
- Для строки `tasks.stage`, отсутствующей в карте (истинно неизвестная/битая/будущая
стадия), `plane_status_label` возвращает нейтральный лейбл (напр. «В работе» или
капитализированный stage), НЕ «To Analyse».
- `created` сохраняет осмысленный «To Analyse» как реальный первый статус.
- `plane_status_label` остаётся never-raise (любой сбой → безопасный лейбл).
### FR-4 — Отражение откатов в строках стадий (BR-4)
- В `render_task_tracker` строка `✅ <стадия>` НЕ отрисовывается для стадии, позиция которой
в конвейере ПОЗЖЕ текущего `tasks.stage`, даже если у её агента есть завершённый `agent_run`.
- Текущая стадия рисуется активной (`🔄`) по существующей логике `is_active_stage`.
- Стадии ДО текущей позиции (фактически пройденные) сохраняют `` со своими метриками.
- Источник порядка стадий — конвейер `STAGE_TRANSITIONS` (а не индекс в `_TRACKER_STAGES`);
конкретный механизм определения позиции — за архитектором. Учесть, что `deploy-staging`
отсутствует в `_TRACKER_STAGES` и `_STAGE_ACTIVE_AGENT` (обе стадии staging/deploy → агент
`deployer`): решение не должно ломать существующий рендер строки «Внедрение».
### FR-5 — Суммирование метрик стадии по попыткам (BR-5)
- Строка стадии показывает СУММУ по всем `agent_runs` агента стадии (по `task_id`):
- 💰 стоимость = `Σ cost_usd`;
- 🔢 токены = `Σ (input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens)`
(вход — через существующий `_input_total`; формат строки `<in>↓/<out>↑` сохранить);
- ⏱ время = `Σ _duration_seconds(started_at, finished_at)` по всем прогонам стадии.
- Тоталы задачи (💰/🔢/⏱ Агенты) остаются суммой по всем стадиям/попыткам и сходятся с
`SUM(agent_runs)` по `task_id` (инвариант сходимости).
- Модель/эффорт/счётчик «попытка N» в строке стадии сохранить (ORCH-087): при N≥2 показывать
актуально (модель — допускается из последнего прогона; согласовать с архитектором).
## 4. Изменения API
Нет. Эндпоинты не затрагиваются (рендер карточки вызывается из конвейера). Диагностический
блок `GET /queue` не меняется.
## 5. Изменения схемы БД
Нет. Используются существующие колонки `agent_runs` (`cost_usd`, `input_tokens`,
`output_tokens`, `cache_read_tokens`, `cache_creation_tokens`, `started_at`, `finished_at`,
`agent`, `task_id`, `exit_code`) и `tasks` (`stage`, `brd_review_started_at/ended_at`).
## 6. Требования к новым/изменённым QG checks
Нет. `QG_CHECKS` / `check_*` / `_parse_*` / `STAGE_TRANSITIONS` не затрагиваются. Изменение
касается только слоя индикации (карточка), не управляющего слоя конвейера.
## 7. Совместимость / регресс
- **Обратная совместимость:** все существующие метки и строки карточки неизменны (NFR-2):
In Review (brd-clock), Awaiting Deploy (`deploy`), Done, live-overlay ветки (Needs Input /
Blocked / Rejected / Cancelled / Confirm Deploy / Deploying / Monitoring), строка
`Подтверждение BRD`, формат строк стадий и тоталов, эффорт-суффикс.
- **Область раската:** косметика карточки для всех проектов общего инстанса (self-hosting +
enduro-trails). Чисто индикативный слой — управляющий конвейер не затронут.
- **Обратимость:** изменение docs/code-only в одном модуле; откат = revert PR. Kill-switch не
требуется (нет нового поведения конвейера; рендер never-raise деградирует безопасно).
- **Артефакты pipeline:** создаются/обновляются стандартные analysis-доки
(`01..04`); на стадии review — `12-review.md`; на testing — `13-test-report.md`. Новых
типов артефактов не вводится.
- **Анти-стейл/трассировка:** правится код, помеченный ORCH-067/ORCH-087 — перед правкой
читать их ADR (`docs/work-items/ORCH-067|ORCH-087/06-adr/`) и не ломать инварианты
(single-card, never-raise, разделение offline-ядра и live-overlay).

View File

@@ -0,0 +1,111 @@
---
work_item: ORCH-091
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-091 — Карточка трекера: статусы, откаты, метрики
Work Item: **ORCH-091** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
(что считается провалом). Reviewer/тестер проверяет их буквально по файлам репозитория и
по выводу тестов.
---
## AC-1 — Полнота карты статусов по `STAGE_TRANSITIONS` (Деф.1 / BR-1)
**Условие:** для КАЖДОГО ключа `src/stages.py::STAGE_TRANSITIONS` `plane_status_label`
возвращает непустой осмысленный лейбл.
- **PASS:** параметризованный тест итерирует по всем ключам `STAGE_TRANSITIONS` и для каждого
(кроме реального `created`) получает непустой лейбл ≠ `_DEFAULT_STATUS_LABEL`-«To Analyse».
Полнота карты выведена программно из `STAGE_TRANSITIONS`, а не статичным списком в тесте.
- **FAIL:** хотя бы одна стадия из `STAGE_TRANSITIONS` отдаёт «To Analyse» (кроме `created`);
либо полнота проверяется захардкоженным списком, не связанным с `STAGE_TRANSITIONS`.
---
## AC-2 — Staging-лейбл для `deploy-staging` (Деф.1 / BR-2)
**Условие:** `stage='deploy-staging'` даёт осмысленный staging-лейбл.
- **PASS:** `plane_status_label` для строки со `stage='deploy-staging'` возвращает осмысленный
staging-лейбл (напр. «Deploying (staging)» / «⏳ Staging»), отличный от «To Analyse» и от
лейбла стадии `deploy` (`⏸️ Awaiting Deploy …`).
- **FAIL:** возвращает «To Analyse», пустую строку, либо лейбл, неотличимый от `deploy`.
---
## AC-3 — Нейтральный фолбэк для неизвестной стадии (Деф.1 / BR-3)
**Условие:** истинно неизвестная/битая стадия → нейтральный фолбэк, never-raise.
- **PASS:** для строки с заведомо несуществующим `stage` (напр. `"__bogus__"`)
`plane_status_label` возвращает нейтральный лейбл (НЕ «To Analyse») и не бросает исключение;
для битого входа (None/нет ключа `stage`) тоже не падает.
- **FAIL:** неизвестная стадия даёт «To Analyse»; либо функция бросает исключение на
битом/неизвестном входе.
---
## AC-4 — Отражение отката в строках стадий (Деф.2 / BR-4)
**Условие:** после rollback `deploy-staging → development` карточка показывает фактическую
позицию.
- **PASS:** для задачи с завершёнными прогонами reviewer/tester/deployer, но текущим
`stage='development'`, `render_task_tracker` рисует Разработку как активную (`🔄`), а
Тестирование и Внедрение — НЕ как `✅ пройдено`. Стадии до development (Анализ, Архитектура)
остаются `✅`.
- **FAIL:** карточка одновременно показывает `✅ Внедрение/Тестирование/Код-ревью` и
`🔄 Разработка` (картина «Внедрение готово ✅, но идёт Разработка»).
---
## AC-5 — Суммирование метрик стадии по попыткам (Деф.3 / BR-5)
**Условие:** стадия с N попытками показывает СУММУ метрик по всем N `agent_runs`.
- **PASS:** для стадии с N>1 `agent_runs` строка стадии показывает Σ времени, Σ токенов
(`input+output+cache_read+cache_creation`) и Σ стоимости по всем N прогонам. На фикстуре
по образцу ORCH-069 (developer: 3 прогона, суммарно ≈ $3.98) строка «Разработка» отражает
≈ $3.98, а не стоимость последнего прогона. Тоталы задачи (💰/🔢/⏱ Агенты) сходятся с
`SUM(agent_runs)` по `task_id` (по стоимости, токенам, длительностям).
- **FAIL:** строка стадии показывает метрики только последнего прогона (занижение); либо
тоталы задачи не сходятся с `SUM(agent_runs)`.
---
## AC-6 — Регресс существующих меток (NFR-2)
**Условие:** существующие индикаторы карточки не изменены.
- **PASS:** In Review (brd-clock, `_IN_REVIEW_LABEL`), Awaiting Deploy (`deploy`), Done,
live-overlay ветки (Needs Input / Blocked / Rejected / Cancelled / Confirm Deploy /
Deploying / Monitoring), строка `Подтверждение BRD`, формат строк стадий/тоталов и
эффорт-суффикс — рендерятся как прежде; существующие тесты карточки зелёные.
- **FAIL:** изменён текст/формат любой из перечисленных меток; падает существующий тест
карточки.
---
## AC-7 — Тесты и документация (G/AC-7)
**Условие:** добавлены тесты и обновлена документация.
- **PASS:** `pytest tests/ -q` зелёный; добавлены тесты на полноту карты стадий (AC-1/2/3),
суммирование метрик (AC-5), отражение отката (AC-4); `CHANGELOG.md` содержит запись
ORCH-091; `render_task_tracker`/`plane_status_label` остаются never-raise.
- **FAIL:** регресс `pytest tests/ -q`; отсутствует любой из обязательных новых тестов; не
обновлён `CHANGELOG.md`.
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / FR-1 |
| AC-2 | BR-2 / FR-2 |
| AC-3 | BR-3 / FR-3 |
| AC-4 | BR-4 / FR-4 |
| AC-5 | BR-5 / FR-5 |
| AC-6 | NFR-2 (регресс) |
| AC-7 | NFR-1 + цель G/AC-7 (тесты, доки, never-raise) |

View File

@@ -0,0 +1,76 @@
work_item: ORCH-091
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
title: "Карточка трекера: полнота статусов, отражение откатов, суммирование метрик по попыткам"
framework: pytest
scope: >
Юнит-покрытие чистых функций src/notifications.py (plane_status_label,
render_task_tracker) и интеграция рендера от состояния БД (tasks + agent_runs).
Вне покрытия: транспорт Telegram (send/edit/delete), live-overlay ветки (сеть),
STAGE_TRANSITIONS/QG/схема БД (не трогаются).
notes: >
Полнота карты статусов должна выводиться программно из src/stages.py::STAGE_TRANSITIONS
(а не из захардкоженного списка стадий). Метрики читаются из таблицы agent_runs:
cost_usd, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
started_at/finished_at. Фикстура-эталон сумм — ORCH-069 (developer: 3 прогона ≈ $3.98).
Полный регресс pytest tests/ -q должен оставаться зелёным; существующие тесты карточки
(test_tracker_status_line, test_telegram_tracker, test_tracker_effort_time) не должны
ломаться.
tests:
- id: TC-01
type: unit
description: "Полнота: для каждого ключа STAGE_TRANSITIONS (программная итерация) plane_status_label возвращает непустой лейбл, не 'To Analyse' (кроме created). AC-1"
module: tests/test_tracker_status_line.py
expected: PASS
- id: TC-02
type: unit
description: "stage='deploy-staging' -> осмысленный staging-лейбл, отличный от 'To Analyse' и от лейбла стадии 'deploy'. AC-2"
module: tests/test_tracker_status_line.py
expected: PASS
- id: TC-03
type: unit
description: "Истинно неизвестная стадия ('__bogus__') -> нейтральный фолбэк (не 'To Analyse'); never-raise на битом/None входе. AC-3"
module: tests/test_tracker_status_line.py
expected: PASS
- id: TC-04
type: unit
description: "Регресс ветки plane_status_label: analysis + открытый brd-clock -> In Review; deploy -> Awaiting Deploy; done -> Done; created -> To Analyse. AC-6"
module: tests/test_tracker_status_line.py
expected: PASS
- id: TC-05
type: integration
description: "Откат deploy-staging->development: задача stage='development' с завершёнными прогонами reviewer/tester/deployer -> Разработка активна (🔄), Тестирование/Внедрение НЕ как ✅; Анализ/Архитектура остаются ✅. AC-4"
module: tests/test_tracker_rollback_metrics.py
expected: PASS
- id: TC-06
type: integration
description: "Суммирование метрик стадии: developer с 3 agent_runs (фикстура ORCH-069) -> строка 'Разработка' показывает Σ стоимости ≈ $3.98, Σ токенов, Σ времени, а не последний прогон. AC-5"
module: tests/test_tracker_rollback_metrics.py
expected: PASS
- id: TC-07
type: integration
description: "Сходимость тоталов: тоталы карточки (💰/🔢/⏱ Агенты) равны SUM(agent_runs) по task_id (cost_usd, токены, длительности) при наличии ретраев. AC-5"
module: tests/test_tracker_rollback_metrics.py
expected: PASS
- id: TC-08
type: integration
description: "render_task_tracker never-raise: битые/частичные строки tasks/agent_runs (NULL timestamps, отсутствующий stage) -> возвращает строку-фолбэк без исключения. NFR-1 / AC-7"
module: tests/test_tracker_rollback_metrics.py
expected: PASS
- id: TC-09
type: unit
description: "Регресс существующих строк карточки: формат строк стадий, эффорт-суффикс (ORCH-087), строка 'Подтверждение BRD', блок тоталов — без изменений. AC-6"
module: tests/test_telegram_tracker.py
expected: PASS

View File

@@ -0,0 +1,193 @@
---
work_item: ORCH-091
stage: architecture
author_agent: architect
status: accepted
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# ADR-001: Карточка трекера — полнота карты статусов, отражение откатов, суммирование метрик по попыткам
Work Item: **ORCH-091** — три верифицированных дефекта live-карточки (`src/notifications.py`)
Стадия: **architecture**
Сквозная регистрация: **N/A, локальное решение задачи** (затронут ровно один модуль
индикативного слоя `src/notifications.py`; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` /
схема БД / транспорт нотификаций — не трогаются; новый компонент/стадия/гейт не вводятся).
## Статус
Accepted
## Контекст
Live Telegram-карточка (ORCH-067/087, единственная карточка на задачу,
`render_task_tracker` / `plane_status_label`) — основной канал наблюдения за конвейером.
BRD/ТЗ объединяют три дефекта, сверенные по коду и БД прода (09.06):
- **Деф.1 — застрявший заголовок «To Analyse».** `_STAGE_STATUS_LABEL`
(`src/notifications.py:940`) содержит 8 ключей (`created/analysis/architecture/development/
review/testing/deploy/done`), а `tasks.stage` принимает ключи `STAGE_TRANSITIONS`
(`src/stages.py:12`) — среди них **`deploy-staging`** (не покрыт) и **`cancelled`** (ORCH-090,
не покрыт). `plane_status_label` (`:1009`) делает `.get(stage, _DEFAULT_STATUS_LABEL)`
непокрытая стадия отдаёт дефолт **«To Analyse»** (`:950`). Из 10 реальных стадий не покрыты
две; дефолт-«To Analyse» — ещё и мина: любая новая стадия даст ложный «первый статус».
- **Деф.2 — ложная картина при откате.** Цикл рендера (`:474505`) выводит `✅`-строку для
каждой стадии `_TRACKER_STAGES`, у чьего агента есть завершённый прогон (`last_done`), **без
учёта позиции стадии относительно текущей**. После отката (`deploy-staging → development`,
ORCH-43; `review → development`, REQUEST_CHANGES) карточка показывает «✅ Внедрение … +
🔄 Разработка» — абсурд.
- **Деф.3 — занижение метрик строки стадии.** `_stage_line` берёт `run = last_done.get(agent)`
(`:475`) — ПОСЛЕДНИЙ прогон, теряя предыдущие попытки. Верифицировано на ORCH-069 (task 54):
developer 3 прогона Σ $3.98, карточка показывала ~$0.00. Блок тоталов задачи (`:388404`) уже
суммирует все прогоны — заниженной остаётся **строка стадии**.
Ключевая структурная сложность (флаг ТЗ §FR-4): `_TRACKER_STAGES` — 6 строк; стадии
`deploy-staging` и `deploy` **схлопнуты** в одну строку «Внедрение» (`stage_key="deploy"`,
агент `deployer`). `_STAGE_ACTIVE_AGENT` тоже не содержит `deploy-staging`. Любое решение по
порядку/позиции обязано не сломать этот сложившийся рендер строки «Внедрение».
## Решение
### Сводка
Три аддитивные правки в `src/notifications.py`, минимизирующие регресс-поверхность:
1. **Полнота карты** — расширить `_STAGE_STATUS_LABEL` недостающими ключами
(`deploy-staging`, `cancelled`); заменить runtime-фолбэк с «To Analyse» на **нейтральный**
(капитализированное имя стадии). Полнота гарантируется **тестом**, итерирующим
`STAGE_TRANSITIONS.keys()` (единый источник истины), а не дублирующим списком.
2. **Отражение откатов** — ввести позицию стадии в конвейере из **порядка `STAGE_TRANSITIONS`**
и гасить `✅`-строку для стадий ПОЗЖЕ текущей позиции. Нормализация `deploy-staging → deploy`
применяется **только** к вычислению текущей позиции (для гейта подавления), логика
`is_active_stage`**без изменений** (нулевой регресс активного рендера).
3. **Суммирование метрик**`_stage_line` агрегирует ВСЕ `agent_runs` агента стадии
(теми же per-run-аккумуляторами, что и блок тоталов) → строгая сходимость с `SUM(agent_runs)`.
Все функции остаются **stateless / never-raise**; любая ошибка деградирует к безопасному
выводу (старое поведение).
### D1 — Полнота `_STAGE_STATUS_LABEL` + нейтральный фолбэк (Деф.1 / FR-1,2,3 / AC-1,2,3)
- Расширить `_STAGE_STATUS_LABEL`, добавив **все** недостающие ключи `STAGE_TRANSITIONS`:
- `"deploy-staging": "Deploying (staging)"` — осмысленный staging-лейбл, согласованный с
моделью статусов ORCH-066/059: **plain-стиль** активной стадии (как `Analysis`/`Testing`,
без `⏸️`-маркера паузы), **отличен** от «To Analyse» и от лейбла `deploy`
(«⏸️ Awaiting Deploy — ожидание Confirm Deploy»). Суффикс «(staging)» снимает коллизию с
prod-overlay «Deploying» (`_LIVE_BRANCH_LABELS['deploying']`). (FR-2 / AC-2.)
- `"cancelled": "Cancelled"` — offline-база для системного терминала ORCH-090. Совпадает с
overlay-лейблом `_LIVE_BRANCH_LABELS['cancelled']` ("Cancelled") → нет конфликта precedence
в `_card_status_label`; offline-путь больше не отдаёт «To Analyse» для отменённой задачи.
- **Runtime-фолбэк** в `plane_status_label`: вместо `_STAGE_STATUS_LABEL.get(stage,
_DEFAULT_STATUS_LABEL)` использовать **нейтральный** лейбл для отсутствующего ключа —
капитализированное имя стадии (напр. `stage.replace("-", " ").title()` → «Deploy Staging»),
с финальным безопасным дефолтом при пустом/битом входе. `created` сохраняет осмысленный
«To Analyse» как реальный первый статус (он **остаётся явным ключом** в карте). (FR-3 / AC-3.)
- `_DEFAULT_STATUS_LABEL` сохраняется как имя для `created` и для безопасной деградации на
истинно-битом входе (`None`/нет ключа `stage`); он **перестаёт** быть фолбэком для «известная
стадия, но нет лейбла» — этот путь теперь нейтрально-капитализированный.
- Спецветка `analysis` + открытый brd-clock → `_IN_REVIEW_LABEL` — **без изменений** (NFR-2).
- **Программная полнота (NFR-3)** обеспечивается тестом, который итерирует
`from src.stages import STAGE_TRANSITIONS` и для каждого ключа (кроме `created`) утверждает
непустой лейбл `≠ _DEFAULT_STATUS_LABEL`. Новая стадия без курируемого лейбла → красный тест.
**Запрещено** в самом модуле автогенерировать лейблы из имён стадий (теряется человеческая
осмысленность) — карта остаётся курируемой, тест лишь гарантирует её покрытие.
### D2 — Отражение откатов: позиция из `STAGE_TRANSITIONS` (Деф.2 / FR-4 / AC-4)
- Ввести в `src/notifications.py` (НЕ в `src/stages.py` — он read-only по ТЗ) лёгкий
индекс-хелпер от **единого источника порядка** `STAGE_TRANSITIONS`:
```python
from .stages import STAGE_TRANSITIONS
_PIPELINE_ORDER = list(STAGE_TRANSITIONS.keys()) # created..done, cancelled
def _pipeline_pos(stage): # never-raise
try:
return _PIPELINE_ORDER.index(stage)
except (ValueError, TypeError):
return len(_PIPELINE_ORDER) # unknown -> «далёкое будущее»
```
- **Нормализация staging→deploy ТОЛЬКО для текущей позиции:**
`effective_stage = "deploy" if stage == "deploy-staging" else stage`;
`current_pos = _pipeline_pos(effective_stage)`. Это отражает, что строка «Внедрение»
представляет фазу deployer'а (staging+prod) как одну — иначе при `stage='deploy-staging'`
строка «Внедрение» (`stage_key="deploy"`, pos 7) была бы ошибочно подавлена (6 < 7).
- **Гейт подавления:** ветка `elif run is not None` (рендер `✅ <стадия>`) срабатывает **только
если** `current_pos >= _pipeline_pos(stage_key)`. Иначе (прогон есть, но стадия ПОЗЖЕ
текущей — откат) строка не выводится. Стадии ДО/НА текущей позиции сохраняют `` (фактически
пройденные).
- **`is_active_stage` — без изменений** (использует «сырой» `stage`, `_STAGE_ACTIVE_AGENT`,
`has_inflight`). Это даёт нулевой регресс активного/«just-finished snapshot» рендера (NFR-2):
при `stage='deploy-staging'` строка «Внедрение» ведёт себя как сегодня (✅ при завершённом
staging-прогоне, иначе ничего) — нормализация затрагивает лишь гейт подавления, не активность.
- Источник позиции — **порядок `STAGE_TRANSITIONS`**, а не индекс в `_TRACKER_STAGES` (NFR-3,
FR-4): добавление/перестановка стадий в движке автоматически корректирует подавление.
- **Условие 🔄 после отката (AC-4):** строка «Разработка» рисуется активной (`🔄`) существующей
логикой `is_active_stage`, которой нужен `has_inflight or run is None`. Реальное
пост-откатное состояние — reviewer/merge-gate ставит developer-job → launcher создаёт строку
`agent_runs` c `finished_at IS NULL` (in-flight) → `🔄`. Фикстура AC-4 обязана содержать
этот in-flight developer-прогон (так выглядит прод после отката). Наш фикс ортогонально
снимает ложные `` со стадий review/testing/Внедрение.
### D3 — Суммирование метрик строки стадии (Деф.3 / FR-5 / AC-5)
- `_stage_line` принимает **список прогонов** агента стадии (готовый
`agent_runs_by_agent.get(agent, [])`) вместо одного `run` и агрегирует **теми же
per-run-формулами, что блок тоталов задачи** (`:388404`):
- 💰 `cost = Σ float(cost_usd or 0)`;
- 🔢 `in = Σ _input_total(usage)` (= Σ(input+cache_read+cache_creation)),
`out = Σ int(output_tokens or 0)`; формат `<in>↓/<out>↑` сохранён;
- ⏱ `dur = Σ _duration_seconds(started_at, finished_at)` (None-прогоны пропускаются, как в
тоталах).
- **Инвариант сходимости:** блок тоталов и строки стадий теперь аккумулируют **по одному и тому
же множеству строк `agent_runs`** и **одними формулами**; каждый агент привязан ровно к одной
строке `_TRACKER_STAGES` (analyst/architect/developer/reviewer/tester/deployer). Поэтому
Σ(показанных+подавленных строк стадий) ≡ тоталы задачи ≡ `SUM(agent_runs)` по `task_id`
(по стоимости/токенам/времени). Подавлённые откатом строки (D2) не рисуются, но их прогоны
**по-прежнему** входят в тоталы — это и есть намеренная семантика отката, инвариант AC-5 не
нарушается (тоталы считают всё; строка стадии — Σ своих прогонов).
- **Модель/эффорт/«попытка N» (ORCH-087, FR-5):** агрегируются метрики, но модель/эффорт
берутся из **последнего** прогона агента (`agent_runs` упорядочены `id ASC` → последний
элемент списка) через существующие `short_model_name` / `_run_effort`; счётчик попыток для
активной строки (`len(agent_runs)`) — без изменений. Формат строки байт-в-байт сохранён (NFR-2).
## Альтернативы
- **Автогенерация лейблов из имён стадий (полностью программная карта)** — отвергнуто: теряется
человеческая осмысленность («deploy-staging» → не лучше «Deploying (staging)»); курируемая
карта + тест полноты дают и читаемость, и анти-рассинхрон.
- **Нормализация `deploy-staging→deploy` во ВСЁМ цикле (включая `is_active_stage`)** —
отвергнуто как первичное решение: меняет активный рендер строки «Внедрение» на стадии
`deploy-staging` (риск регресса существующих тестов, NFR-2/AC-6). Нормализация ограничена
гейтом подавления — минимальная поверхность.
- **Позиция стадии из индекса `_TRACKER_STAGES`** — отвергнуто: `_TRACKER_STAGES` не содержит
`deploy-staging`/`cancelled` и не является источником истины о порядке конвейера (нарушает
NFR-3). Источник — `STAGE_TRANSITIONS`.
- **Изменение `_TRACKER_STAGES`/`_STAGE_ACTIVE_AGENT` (добавить deploy-staging-строку)** —
отвергнуто: вне объёма BRD (формат строк неизменен, NFR-2), расширяет регресс-поверхность.
## Последствия
- **+** Заголовок честен на всех стадиях (вкл. `deploy-staging`, `cancelled`); будущая стадия
не даёт ложный «To Analyse» (нейтральный фолбэк + тест полноты).
- **+** Карточка не «лжёт» после отката: `` снимается со стадий ПОЗЖЕ текущей позиции.
- **+** Метрики строки стадии = Σ всех попыток; строгая сходимость с `SUM(agent_runs)`.
- **+** Источник порядка/полноты — `STAGE_TRANSITIONS` (программно), анти-рассинхрон на будущее.
- **** Новая read-only связь `notifications.py → stages.STAGE_TRANSITIONS` (порядок+ключи).
Митигейшн: импорт ключей, `stages.py` не изменяется (разрешено ТЗ §2); `_pipeline_pos`
never-raise (unknown → «далёкое будущее» = старое поведение, ✅ не пере-подавляется).
- **** При `stage='deploy-staging'` строка «Внедрение» может показать `` по завершённому
staging-прогону (до prod-деплоя). Это **сохранённое** поведение (NFR-2), не регресс и не
дефект по BRD; нормализация затрагивает только подавление, не активность.
- **Откат:** изменение docs/code-only в одном модуле + тесты → `git revert` PR. Kill-switch не
требуется (нет нового поведения конвейера; рендер never-raise деградирует безопасно).
## Ссылки
- BRD: `docs/work-items/ORCH-091/01-brd.md`
- TRZ: `docs/work-items/ORCH-091/02-trz.md`
- Acceptance: `docs/work-items/ORCH-091/03-acceptance-criteria.md`
- Tech-risks: `docs/work-items/ORCH-091/10-tech-risks.md`
- Сверено по коду: `src/notifications.py` (`_STAGE_STATUS_LABEL:940`, `_DEFAULT_STATUS_LABEL:950`,
`plane_status_label:990`, `render_task_tracker:333`, `_stage_line:445`, `_TRACKER_STAGES:233`,
`_STAGE_ACTIVE_AGENT:248`, totals `:388-404`), `src/stages.py::STAGE_TRANSITIONS:12`,
`src/usage.py::_input_total:348`.
- Инварианты, которые НЕЛЬЗЯ ломать (прочитаны перед правкой): ORCH-067/ORCH-087
(`docs/work-items/ORCH-067|ORCH-087/06-adr/`) — single-card, never-raise, разделение
offline-ядра и live-overlay; ORCH-090 (`adr-0026`) — терминал `cancelled`.

View File

@@ -0,0 +1,36 @@
---
work_item: ORCH-091
stage: architecture
author_agent: architect
status: accepted
created_at: 2026-06-09
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-091 — Карточка трекера (статусы, откаты, метрики)
Work Item: **ORCH-091** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | Регресс существующих меток/строк при правке цикла рендера (In Review, Awaiting Deploy, Done, эффорт-суффикс, формат строк/тоталов) | Сред. | Сред. | `is_active_stage` не трогаем; нормализация только в гейте подавления (D2); формат `_stage_line` байт-в-байт; зелёные `tests/test_tracker_*` + `test_telegram_tracker` (AC-6). |
| TR-2 | Рассинхрон карты статусов с `STAGE_TRANSITIONS` в будущем (новая стадия без лейбла) | Сред. | Низ. | Полнота — тест по `STAGE_TRANSITIONS.keys()` (NFR-3); нейтральный фолбэк вместо «To Analyse» (D1) → даже без лейбла не «лжёт». |
| TR-3 | Неверная точка отсчёта позиции стадии → неверное снятие/сохранение `✅` (особенно схлопывание `deploy-staging`/`deploy` в строку «Внедрение») | Сред. | Сред. | Позиция из порядка `STAGE_TRANSITIONS`; нормализация `deploy-staging→deploy` только для current-pos (D2); сценарные тесты отката `deploy-staging→development` и `review→development` (AC-4). |
| TR-4 | Расхождение метрик строки стадии с тоталами задачи (двойной/потерянный учёт) | Низ. | Сред. | Строка и тоталы используют ОДНИ формулы (`_input_total`/`_duration_seconds`/`cost_usd`) над ОДНИМ множеством `agent_runs`; тест сходимости Σ(строки) == `SUM(agent_runs)` по `task_id` (AC-5). |
| TR-5 | Исключение в `render_task_tracker`/`plane_status_label` блокирует индикацию | Низ. | Сред. | Контракт never-raise сохранён; `_pipeline_pos` never-raise (unknown → «далёкое будущее» = старое поведение); деградация к безопасному выводу (NFR-1/AC-3,7). |
| TR-6 | Новая import-связь `notifications.py → stages` вводит цикл импорта | Низ. | Низ. | `stages.py` — лист без обратных зависимостей на `notifications`; импорт ключей словаря, не функций; `stages.py` не изменяется (ТЗ §2). |
| TR-7 | Фикстура AC-4 без in-flight developer-прогона → строка «Разработка» не `🔄` | Низ. | Низ. | ADR D2 фиксирует: пост-откатный `🔄` требует строки `agent_runs` c `finished_at IS NULL`; тест-план обязан включать такой прогон (реальное прод-состояние после relaunch). |
## Сводный вывод
Доминирующий класс — **риски регресса индикативного слоя** (TR-1/TR-3) и **сходимости метрик**
(TR-4). Все смягчаются тестами и минимальной поверхностью правок (один модуль, без затрагивания
`STAGE_TRANSITIONS`/`QG_CHECKS`/схемы БД/транспорта). Эскалация `arch:major-change` **не нужна**:
изменение локально, обратимо `git revert`, never-raise, kill-switch не требуется. Возврат в анализ
**не требуется** — BRD/ТЗ полны и реализуемы без нарушения принципов. Остаточный риск для
прод-конвейера (self-hosting) — **низкий**: слой чисто индикативный, управляющий конвейер
(стадии/гейты/очередь) не затрагивается, рендер деградирует безопасно.

View File

@@ -0,0 +1,85 @@
---
verdict: APPROVED
work_item: ORCH-091
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-09
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-091
version: 1
---
# Review ORCH-091
## Summary
PR закрывает три верифицированных дефекта рендера live-карточки трекера
(`src/notifications.py`, ORCH-067/087): (Д1) застрявший заголовок «To Analyse» из-за неполноты
`_STAGE_STATUS_LABEL`; (Д2) ложные `✅`-строки стадий после отката конвейера; (Д3) занижение
метрик строки стадии (последний прогон вместо суммы попыток). Изменение **аддитивное,
indication-only, never-raise**: затронут ровно один src-модуль (`src/notifications.py`),
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / транспорт нотификаций / схема БД — не тронуты.
Проверка по четырём осям пройдена:
- **Соответствие ТЗ** — все FR-1…FR-5 реализованы; AC-1…AC-7 выполнены буквально.
- **Соответствие ADR** — реализация 1:1 с `06-adr/ADR-001` (D1/D2/D3); read-only-связь
`notifications.py → stages.STAGE_TRANSITIONS` оформлена как указано; `is_active_stage` не тронут.
- **Качество кода** — `_pipeline_pos` / `_neutral_stage_label` never-raise; докстринги и
трассировочные ORCH-091-комментарии присутствуют; полный регресс зелёный (1370).
- **Документация** — обновлена в том же PR (см. ниже).
### Проверенные инварианты
- **Трассировка ORCH-067/087** (правка маркированного кода): инварианты single-card, never-raise,
разделение offline-ядра/live-overlay сохранены — подтверждено ADR (прочитаны перед правкой) и
зелёным регрессом `test_tracker_status_line.py`.
- **Терминал `cancelled` (ORCH-090, adr-0026)**: добавлен offline-лейбл `cancelled → "Cancelled"`,
совпадает с overlay `_LIVE_BRANCH_LABELS['cancelled']` → нет конфликта precedence.
- **Полнота карты от источника истины** — тест `test_orch091_tc01_*` параметризован по
`STAGE_TRANSITIONS.keys()` (не статичный список) → NFR-3 выполнен.
- **Сходимость метрик** — `_stage_line` использует те же per-run-формулы, что блок тоталов;
тест `test_tc07_*` проверяет сходимость с `SUM(agent_runs)` и Σ(строк стадий) ≡ тоталы на done.
- **Нормализация `deploy-staging → deploy`** ограничена гейтом подавления (не затрагивает
активный рендер строки «Внедрение») — подтверждено `test_tc05_deploy_staging_keeps_deployer_row`.
- **Отсутствие циркулярного импорта** — `import src.notifications; import src.stages` → OK.
- **ORCH-079 (обзорные доки)** — `README.md` «Известные ограничения» НЕ содержит пункта о
дефектах карточки трекера → закрывать/обновлять нечего; gate не нарушен.
## Findings
### P0 — Blocker
- Нет.
### P1 — Must fix
- Нет.
### P2 — Should fix
- Нет.
### P3 — Nice-to-have (не блокирует)
- [ ] `from .stages import STAGE_TRANSITIONS` размещён в середине модуля (`src/notifications.py`
после `_STAGE_ACTIVE_AGENT`, с `# noqa: E402`). Размещение намеренно и документировано
комментарием, циркулярного импорта нет; вынос в шапку модуля — косметическая необязательная
уборка на будущее.
## Документация
Обновлена в том же PR (golden source синхронен с кодом):
- **`CHANGELOG.md`** — запись ORCH-091 (`fix`) с описанием трёх дефектов, тестов и отката. ✅
- **`docs/architecture/internals.md`** §7 — описаны откат-подавление `✅`, суммирование метрик
и полнота `_STAGE_STATUS_LABEL`. ✅
- **`docs/architecture/README.md`** (Notifications / Live-tracker) — добавлен блок «ORCH-091
(индикация-only)» с тремя правками и ссылкой на ADR. ✅ (внесено архитектором, присутствует в PR)
- **ADR** — `docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md`
(status: accepted), решения D1/D2/D3, альтернативы, последствия. ✅
- **`README.md` (root)** — обновление не требуется: ни один пункт «Известные ограничения» не
закрывается данным PR (ORCH-079 gate соблюдён). ✅
Изменения `src/` сопровождены соответствующим обновлением документации → ось «документация»
пройдена; основание для `REQUEST_CHANGES` по этой оси отсутствует.
## Вердикт
`APPROVED` — нет findings уровня P0/P1; код, тесты и документация согласованы; инварианты
ORCH-067/087/090 и NFR-2/NFR-3 сохранены; полный регресс `pytest tests/ -q` зелёный (1370 passed).

View File

@@ -0,0 +1,98 @@
---
result: PASS # PASS | FAIL — машинный вердикт, UPPERCASE
work_item: ORCH-091
stage: testing
author_agent: tester
status: pass
created_at: 2026-06-09
model_used: claude-opus-4-8
type: test-report
work_item_id: ORCH-091
---
# Test Report — ORCH-091
BUG: заголовок-строка live-карточки трекера застревает на «To Analyse» на
`stage=deploy-staging` (нет ключа в `_STAGE_STATUS_LABEL`) + ложные `✅`-строки после
отката + занижение метрик строки стадии (последний прогон вместо суммы попыток).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-091-bug-to-analyse-stage-deploy-st`
- Ветка: `feature/ORCH-091-bug-to-analyse-stage-deploy-st`
- Дата: 2026-06-09
## Предусловия
- Вердикт reviewer (`12-review.md`): **APPROVED** (P0=0, P1=0) ✅
- Тесты прогнаны из worktree ветки задачи (не из общего `/repos/orchestrator`) ✅
## Smoke API (read-only)
| Эндпоинт | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
| `GET /status` | 200, активные задачи отдаются (ORCH-091 = `testing`) — OK |
| `GET /queue` | 200; блок `serial_gate` присутствует (ORCH-088) ✅; блок `auto_labels` присутствует (ORCH-089) ✅ |
`serial_gate.per_repo.orchestrator.active_task = ORCH-091/testing`, регресс смока отсутствует.
## Покрытие тест-плана (`04-test-plan.yaml`) ↔ критерии приёмки (`03-acceptance-criteria.md`)
| TC ID | Описание | AC | Тест(ы) | Результат |
|-------|----------|----|---------|-----------|
| TC-01 | Полнота карты: каждый ключ `STAGE_TRANSITIONS` (программная итерация) → непустой лейбл ≠ «To Analyse» (кроме `created`) | AC-1 | `test_tracker_status_line::test_orch091_tc01_every_stage_has_meaningful_label[*]` (9 параметров) + `test_orch091_tc01_created_stays_to_analyse` | PASS |
| TC-02 | `stage='deploy-staging'` → осмысленный staging-лейбл ≠ «To Analyse» и ≠ лейбла `deploy` | AC-2 | `test_tracker_status_line::test_orch091_tc02_deploy_staging_label` | PASS |
| TC-03 | Неизвестная стадия (`__bogus__`) → нейтральный фолбэк (не «To Analyse»); never-raise на битом/None входе | AC-3 | `test_orch091_tc03_unknown_stage_neutral_not_to_analyse` + `test_orch091_tc03_cancelled_offline_label` + `test_tc09c_plane_status_label_never_raises` | PASS |
| TC-04 | Регресс ветвей `plane_status_label`: analysis+brd-clock→In Review; deploy→Awaiting Deploy; done→Done; created→To Analyse | AC-6 | `test_tracker_status_line::test_tc06_stage_to_plane_status[*]` (8) + `test_tc07_in_review_from_brd_clock` + `test_tc08_awaiting_deploy_offline` | PASS |
| TC-05 | Откат `deploy-staging→development`: Разработка активна (`🔄`), Тестирование/Внедрение НЕ `✅`; Анализ/Архитектура остаются `✅` | AC-4 | `test_tracker_rollback_metrics::test_tc05_rollback_suppresses_later_stage_checkmarks` + `test_tc05_forward_progress_keeps_earlier_checkmarks` + `test_tc05_deploy_staging_keeps_deployer_row` | PASS |
| TC-06 | Суммирование метрик: developer с 3 `agent_runs` (фикстура ORCH-069) → строка «Разработка» = Σ стоимости ≈ $3.98, Σ токенов, Σ времени | AC-5 | `test_tracker_rollback_metrics::test_tc06_stage_line_sums_all_developer_runs` | PASS |
| TC-07 | Сходимость тоталов карточки (💰/🔢/⏱ Агенты) с `SUM(agent_runs)` по `task_id` при ретраях | AC-5 | `test_tc07_totals_converge_with_sum_agent_runs` + `test_tc07_sum_of_stage_lines_equals_totals_on_done` | PASS |
| TC-08 | `render_task_tracker` never-raise: NULL timestamps / отсутствующий stage → строка-фолбэк без исключения | AC-7 / NFR-1 | `test_tc08_render_survives_null_timestamps_and_runs` + `test_tc08_render_survives_bogus_stage` | PASS |
| TC-09 | Регресс строк карточки: формат строк стадий, эффорт-суффикс (ORCH-087), «Подтверждение BRD», блок тоталов — без изменений | AC-6 | `test_telegram_tracker.py` + `test_tracker_effort_time.py` (эффорт по ролям, capped review-time, done-time labels) — все зелёные | PASS |
Каждый TC из `04-test-plan.yaml` выполнен и сопоставлен с критериями `03-acceptance-criteria.md`.
## Вывод pytest
```
$ cd /repos/_wt/orchestrator/feature_ORCH-091-bug-to-analyse-stage-deploy-st
$ pytest tests/ -v --tb=short
tests/test_tracker_rollback_metrics.py::test_tc05_rollback_suppresses_later_stage_checkmarks PASSED
tests/test_tracker_rollback_metrics.py::test_tc05_forward_progress_keeps_earlier_checkmarks PASSED
tests/test_tracker_rollback_metrics.py::test_tc05_deploy_staging_keeps_deployer_row PASSED
tests/test_tracker_rollback_metrics.py::test_tc06_stage_line_sums_all_developer_runs PASSED
tests/test_tracker_rollback_metrics.py::test_tc07_totals_converge_with_sum_agent_runs PASSED
tests/test_tracker_rollback_metrics.py::test_tc07_sum_of_stage_lines_equals_totals_on_done PASSED
tests/test_tracker_rollback_metrics.py::test_tc08_render_survives_null_timestamps_and_runs PASSED
tests/test_tracker_rollback_metrics.py::test_tc08_render_survives_bogus_stage PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[analysis] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[architecture] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[development] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[review] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[testing] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[deploy-staging] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[deploy] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[done] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_every_stage_has_meaningful_label[cancelled] PASSED
tests/test_tracker_status_line.py::test_orch091_tc01_created_stays_to_analyse PASSED
tests/test_tracker_status_line.py::test_orch091_tc02_deploy_staging_label PASSED
tests/test_tracker_status_line.py::test_orch091_tc03_unknown_stage_neutral_not_to_analyse PASSED
tests/test_tracker_status_line.py::test_orch091_tc03_cancelled_offline_label PASSED
... (полный набор регресса трекера/usage/webhooks/verdict-status зелёный)
======================= 1370 passed, 1 warning in 39.33s =======================
```
(1 warning — PydanticDeprecatedSince20 в `src/config.py:8`, преэкзистный, не связан с ORCH-091.)
## Итог
PASS
- Все 1370 тестов зелёные; новые тесты ORCH-091 (TC-01…TC-08) присутствуют и проходят.
- Каждый TC из тест-плана выполнен и сопоставлен с AC-1…AC-7.
- Smoke read-only OK; блоки `serial_gate` и `auto_labels` присутствуют в `GET /queue` (без регресса).
- Изменение indication-only / never-раise; регресс существующих меток карточки (AC-6) подтверждён.
**Вердикт: `result: PASS`** → задача переходит на `deploy-staging`.

View File

@@ -0,0 +1,36 @@
---
staging_status: SUCCESS
work_item: ORCH-091
stage: deploy-staging
author_agent: deployer
status: success
created_at: 2026-06-09
model_used: claude-opus-4-8
timestamp: 2026-06-09T19:07:24Z
base_url: http://localhost:8501
---
# Staging Gate Log
> Машинный вердикт читается ТОЛЬКО из `staging_status:` во frontmatter. Реален для self-hosting
> (`orchestrator`); для прочих репо гейт — N/A (ORCH-35). `SUCCESS` → дальше; `FAILED` → откат.
Staging test suite completed against the live `orchestrator-staging` стенд (8501). Запуск
канонический — внутри контейнера `orchestrator-staging` через `docker exec`
(ORCH-048, ADR-001), mode=stub. **Exit code 0 → `staging_status: SUCCESS`.**
All REAL pipeline checks (Block A SMOKE, Block B ACCESS, C7/C8) passed. The two sandbox-infra
checks C9a/C9b failed and were **waived** under ORCH-061 tolerance (depend on SANDBOX bot accounts
being project members, not on the pipeline) — script still exited 0.
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```
## Results
- **Block A (SMOKE)**: A1 `/health`→200 ok · A2 `/queue`→200 (counts/max_concurrency/resilience) · A3 `ORCH_STAGING=true`. PASS.
- **Block B (ACCESS)**: B4 Plane sandbox accessible (sandbox=YES) · B5 Gitea `orchestrator-sandbox` push=true · B6 Registry isolation (sandbox present, prod ET/ORCH absent). PASS.
- **Block C (E2E, stub)**: C7 create issue in Plane SANDBOX (HTTP 201) · C8 trigger pipeline `/webhook/plane` (accepted) PASS; C9a branch / C9b analyst-job — FAIL, **waived** (sandbox-infra). CLEANUP отработал (Plane issue удалён, ветки не было).
RESULT: 8/10 checks PASS. REAL failed: **none**. SANDBOX_INFRA failed (waived): C9a, C9b.

View File

@@ -254,6 +254,28 @@ _STAGE_ACTIVE_AGENT = {
"deploy": "deployer",
}
# ORCH-091 (D2): pipeline order is read (read-only) from the single source of
# truth src/stages.py::STAGE_TRANSITIONS — NOT from _TRACKER_STAGES (which lacks
# deploy-staging/cancelled and is not authoritative about ordering, NFR-3). Used
# to suppress the "✅ <stage>" line for a stage positioned AFTER the task's
# current stage (a rollback, e.g. deploy-staging -> development), which otherwise
# rendered the absurd "✅ Внедрение … + 🔄 Разработка".
from .stages import STAGE_TRANSITIONS # noqa: E402
_PIPELINE_ORDER = list(STAGE_TRANSITIONS.keys())
def _pipeline_pos(stage) -> int:
"""Index of ``stage`` in the pipeline order; unknown -> "far future".
Never raises. An unknown/broken stage maps past the end so it is never
spuriously suppressed (degrades to the pre-ORCH-091 behaviour: ✅ kept).
"""
try:
return _PIPELINE_ORDER.index(stage)
except (ValueError, TypeError):
return len(_PIPELINE_ORDER)
def _fmt_minutes(seconds) -> str:
"""Render a duration in whole minutes: 0..59s -> '<1м', else '<n>м'."""
@@ -442,23 +464,42 @@ def render_task_tracker(task_id: int) -> str:
except Exception:
pass
def _stage_line(label, run):
usage = {
"input_tokens": run["input_tokens"],
"cache_read_tokens": run["cache_read_tokens"],
"cache_creation_tokens": run["cache_creation_tokens"],
}
in_tok = fmt_tokens(_input_total(usage))
out_tok = fmt_tokens(run["output_tokens"])
cost = fmt_cost(run["cost_usd"])
dur = _fmt_minutes(_duration_seconds(run["started_at"], run["finished_at"]))
model = short_model_name(run["model"])
def _stage_line(label, stage_runs):
# ORCH-091 (D3): aggregate ALL of the stage agent's runs (retries
# included) with the SAME per-run formulas as the task totals block
# (:388-404) -> the stage line converges with SUM(agent_runs) instead of
# showing only the last run (which understated a multi-attempt stage:
# ORCH-069 developer \u03a3 $3.98 rendered as ~$0.00). Each agent maps to
# exactly one _TRACKER_STAGES row, so \u03a3(stage lines) \u2261 task totals.
in_sum = 0
out_sum = 0
cost_sum = 0.0
dur_sum = 0
for run in stage_runs:
usage = {
"input_tokens": run["input_tokens"],
"cache_read_tokens": run["cache_read_tokens"],
"cache_creation_tokens": run["cache_creation_tokens"],
}
in_sum += _input_total(usage)
out_sum += int(run["output_tokens"] or 0)
cost_sum += float(run["cost_usd"] or 0.0)
d = _duration_seconds(run["started_at"], run["finished_at"])
if d is not None:
dur_sum += d
in_tok = fmt_tokens(in_sum)
out_tok = fmt_tokens(out_sum)
cost = fmt_cost(cost_sum)
dur = _fmt_minutes(dur_sum)
# Model/effort/"\u043f\u043e\u043f\u044b\u0442\u043a\u0430 N" come from the LAST run (agent_runs are id ASC).
last = stage_runs[-1] if stage_runs else None
model = short_model_name(last["model"]) if last is not None else ""
model_suffix = f" \u00b7 {model}" if model else ""
# ORCH-087 (BR-EFF): render the resolved --effort next to the model
# ("\u00b7 opus-4-8 \u00b7 xhigh"). Stamped at launch in agent_runs.effort; empty /
# missing -> suffix omitted (like the model suffix). Historical rows with
# NULL effort fall back to the config-resolved effort for the agent.
effort = _run_effort(run)
effort = _run_effort(last) if last is not None else ""
effort_suffix = f" \u00b7 {effort}" if effort else ""
return (
f"\u2705 {label:<13} {dur} \u00b7 "
@@ -471,6 +512,14 @@ def render_task_tracker(task_id: int) -> str:
brd_ended = task["brd_review_ended_at"]
review_seconds = _duration_seconds(brd_started, brd_ended)
# ORCH-091 (D2): the task's current position in the pipeline, used to suppress
# \u2705-lines for stages POSITIONED AFTER it (a rollback). The deploy-staging ->
# deploy normalization is applied ONLY here (not to is_active_stage): the
# collapsed "\u0412\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u0435" row carries stage_key="deploy" (pos 7); on
# stage='deploy-staging' (pos 6) the row would otherwise be wrongly suppressed.
effective_stage = "deploy" if stage == "deploy-staging" else stage
current_pos = _pipeline_pos(effective_stage)
for stage_key, label, agent in _TRACKER_STAGES:
run = last_done.get(agent)
# The stage is "in progress" only when it is the task's current stage AND
@@ -500,9 +549,14 @@ def render_task_tracker(task_id: int) -> str:
lines.append(
f"\U0001f504 {label:<13} \u2026 \u00b7 \u0438\u0434\u0451\u0442"
)
elif run is not None:
lines.append(_stage_line(label, run))
# else: not started yet -> not shown.
elif run is not None and current_pos >= _pipeline_pos(stage_key):
# ORCH-091 (D2): show ✅ only for stages AT or BEFORE the current
# position. A finished run on a stage POSITIONED AFTER the current one
# (rollback, e.g. deploy-staging->development) is suppressed — its runs
# still count in the task totals (intended rollback semantics). Pass the
# FULL run list so the line aggregates all attempts (D3).
lines.append(_stage_line(label, agent_runs))
# else: not started yet, or rolled back past -> not shown.
# Insert the BRD review line right after Analysis.
if stage_key == "analysis" and brd_started:
@@ -944,8 +998,16 @@ _STAGE_STATUS_LABEL = {
"development": "Development",
"review": "Code-Review",
"testing": "Testing",
# ORCH-091 (D1): deploy-staging was missing -> the card froze on "To Analyse".
# Plain-style active label (like Analysis/Testing, no ⏸️ pause marker); the
# "(staging)" suffix keeps it distinct from the prod-overlay "Deploying"
# (_LIVE_BRANCH_LABELS['deploying']) and from the deploy stage's pause label.
"deploy-staging": "Deploying (staging)",
"deploy": "⏸️ Awaiting Deploy — ожидание Confirm Deploy",
"done": "Done",
# ORCH-091 (D1): offline base for the ORCH-090 system-terminal. Matches the
# overlay label _LIVE_BRANCH_LABELS['cancelled'] -> no precedence conflict.
"cancelled": "Cancelled",
}
_DEFAULT_STATUS_LABEL = "To Analyse"
_IN_REVIEW_LABEL = (
@@ -987,6 +1049,25 @@ def _row_get(row, key, default=None):
return default
def _neutral_stage_label(stage) -> str:
"""ORCH-091 (D1): neutral fallback for a stage NOT in _STAGE_STATUS_LABEL.
A genuinely unknown / future / broken stage gets a capitalized stage name
("deploy-staging" -> "Deploy Staging") instead of the misleading "To Analyse"
(which read as a false "first status"). Empty / unparseable -> the safe
_DEFAULT_STATUS_LABEL. Never raises. NOTE: the curated map stays the source of
human-meaningful labels; this is only the safety net for unmapped stages
(FR-3 / AC-3).
"""
try:
s = str(stage).strip()
if not s:
return _DEFAULT_STATUS_LABEL
return s.replace("-", " ").title()
except Exception:
return _DEFAULT_STATUS_LABEL
def plane_status_label(task_row) -> str:
"""ORCH-067 (Р-1, layer 1): current Plane status label for the card header.
@@ -1006,7 +1087,13 @@ def plane_status_label(task_row) -> str:
ended = _row_get(task_row, "brd_review_ended_at")
if started and not ended:
return _IN_REVIEW_LABEL
return _STAGE_STATUS_LABEL.get(stage, _DEFAULT_STATUS_LABEL)
# ORCH-091 (D1/FR-3): a mapped stage keeps its curated label; an UNMAPPED
# (future/unknown) stage degrades to a neutral capitalized label, NOT the
# misleading "To Analyse". 'created' stays an explicit key -> "To Analyse".
label = _STAGE_STATUS_LABEL.get(stage)
if label:
return label
return _neutral_stage_label(stage)
except Exception:
return _DEFAULT_STATUS_LABEL

View File

@@ -0,0 +1,283 @@
"""ORCH-091 — Group 2 (D2/D3): rollback reflection + stage-metric summation.
Covers TC-05..TC-08 from 04-test-plan.yaml. The render path is pure DB (no
network); a temp SQLite holds tasks + agent_runs.
TC-05 / AC-4 — rollback deploy-staging->development: Development active (🔄),
Testing/Внедрение NOT shown ✅, Анализ/Архитектура stay ✅.
TC-06 / AC-5 — stage line sums ALL of an agent's runs (ORCH-069 developer
3 runs ≈ $3.98), not the last run.
TC-07 / AC-5 — task totals (💰/🔢/⏱) converge with SUM(agent_runs).
TC-08 / AC-7 — render_task_tracker never raises on broken/partial rows.
"""
import os
import tempfile
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
_test_db = os.path.join(tempfile.gettempdir(), "test_orchestrator_rollback_metrics.db")
os.environ["ORCH_DB_PATH"] = _test_db
import pytest # noqa: E402
import src.db as db_module # noqa: E402
from src.db import init_db, get_db # noqa: E402
from src import notifications as N # noqa: E402
from src.usage import fmt_cost, fmt_tokens, _input_total # noqa: E402
@pytest.fixture(autouse=True)
def setup_db(monkeypatch):
monkeypatch.setattr(db_module.settings, "db_path", _test_db, raising=False)
if os.path.exists(_test_db):
os.unlink(_test_db)
init_db()
# Render-only: keep the live overlay off (offline core under test).
monkeypatch.setattr(N._get_settings(), "tracker_live_status", False, raising=False)
yield
if os.path.exists(_test_db):
os.unlink(_test_db)
def _mk_task(stage="development", wid="ORCH-091", title="rollback/metrics",
created=None, updated=None):
conn = get_db()
cur = conn.execute(
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title) "
"VALUES (?,?,?,?,?,?)",
("p1", wid, "orchestrator", "feature/ORCH-091-x", stage, title),
)
tid = cur.lastrowid
if created or updated:
conn.execute(
"UPDATE tasks SET created_at=COALESCE(?, created_at), "
"updated_at=COALESCE(?, updated_at) WHERE id=?",
(created, updated, tid),
)
conn.commit()
conn.close()
return tid
def _mk_run(tid, agent, started, finished, *, model="tokenator/claude-opus-4-8",
in_tok=10, out_tok=5, cache_read=0, cache_creation=0, cost=0.0,
effort=None, exit_code=0):
conn = get_db()
conn.execute(
"INSERT INTO agent_runs (task_id, agent, started_at, finished_at, "
"exit_code, input_tokens, output_tokens, cache_read_tokens, "
"cache_creation_tokens, cost_usd, model, effort) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(tid, agent, started, finished, exit_code, in_tok, out_tok, cache_read,
cache_creation, cost, model, effort),
)
conn.commit()
conn.close()
def _stage_line(text, label):
"""The single '✅ <label> ...' line for a stage, or None."""
for ln in text.splitlines():
if ln.startswith(f"{label}"):
return ln
return None
def _has_active(text, label):
"""True if the '🔄 <label> ...' active line is present."""
return any(ln.startswith(f"🔄 {label}") for ln in text.splitlines())
# =========================================================================== #
# TC-05 / AC-4 — rollback reflection (deploy-staging -> development)
# =========================================================================== #
def test_tc05_rollback_suppresses_later_stage_checkmarks():
"""A task back on stage='development' after later stages ran: Development is
active (🔄), and Тестирование/Внедрение/Код ревью are NOT shown as ✅, while
earlier stages (Анализ/Архитектура) stay ✅."""
tid = _mk_task(stage="development")
# Earlier stages finished.
_mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00")
_mk_run(tid, "architect", "2026-06-04 09:10:00", "2026-06-04 09:20:00")
# First development pass finished, then later stages ran...
_mk_run(tid, "developer", "2026-06-04 09:20:00", "2026-06-04 09:40:00", cost=1.0)
_mk_run(tid, "reviewer", "2026-06-04 09:40:00", "2026-06-04 09:50:00")
_mk_run(tid, "tester", "2026-06-04 09:50:00", "2026-06-04 10:00:00")
_mk_run(tid, "deployer", "2026-06-04 10:00:00", "2026-06-04 10:10:00")
# ...then a rollback re-launched developer -> in-flight run (finished_at NULL).
_mk_run(tid, "developer", "2026-06-04 10:20:00", None, exit_code=None, cost=0.0)
text = N.render_task_tracker(tid)
# Development active, not a ✅.
assert _has_active(text, "Разработка"), text
# Later-than-current stages: no ✅ line (the rollback is honestly reflected).
assert _stage_line(text, "Код ревью") is None, text
assert _stage_line(text, "Тестирование") is None, text
assert _stage_line(text, "Внедрение") is None, text
# Earlier stages still ✅.
assert _stage_line(text, "Анализ") is not None, text
assert _stage_line(text, "Архитектура") is not None, text
def test_tc05_forward_progress_keeps_earlier_checkmarks():
"""Regression guard: normal forward progress (no rollback) still shows all
earlier stages ✅ — the suppression gate only fires for stages AFTER current."""
tid = _mk_task(stage="testing")
_mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00")
_mk_run(tid, "architect", "2026-06-04 09:10:00", "2026-06-04 09:20:00")
_mk_run(tid, "developer", "2026-06-04 09:20:00", "2026-06-04 09:40:00")
_mk_run(tid, "reviewer", "2026-06-04 09:40:00", "2026-06-04 09:50:00")
# tester in-flight on the testing stage.
_mk_run(tid, "tester", "2026-06-04 09:50:00", None, exit_code=None)
text = N.render_task_tracker(tid)
assert _stage_line(text, "Анализ") is not None
assert _stage_line(text, "Архитектура") is not None
assert _stage_line(text, "Разработка") is not None
assert _stage_line(text, "Код ревью") is not None
assert _has_active(text, "Тестирование")
def test_tc05_deploy_staging_keeps_deployer_row():
"""Normalization: on stage='deploy-staging' the collapsed 'Внедрение' row
(stage_key='deploy') is NOT wrongly suppressed by the rollback gate."""
tid = _mk_task(stage="deploy-staging")
_mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00")
_mk_run(tid, "architect", "2026-06-04 09:10:00", "2026-06-04 09:20:00")
_mk_run(tid, "developer", "2026-06-04 09:20:00", "2026-06-04 09:40:00")
_mk_run(tid, "reviewer", "2026-06-04 09:40:00", "2026-06-04 09:50:00")
_mk_run(tid, "tester", "2026-06-04 09:50:00", "2026-06-04 10:00:00")
# staging deploy finished (deployer agent, collapsed into Внедрение).
_mk_run(tid, "deployer", "2026-06-04 10:00:00", "2026-06-04 10:10:00")
text = N.render_task_tracker(tid)
# Внедрение must NOT be suppressed (preserved pre-ORCH-091 behaviour).
assert _stage_line(text, "Внедрение") is not None, text
assert _stage_line(text, "Тестирование") is not None, text
# =========================================================================== #
# TC-06 / AC-5 — stage-metric summation over retries (ORCH-069 fixture)
# =========================================================================== #
def test_tc06_stage_line_sums_all_developer_runs():
"""developer with 3 runs (ORCH-069: Σ ≈ $3.98) -> the 'Разработка' line shows
Σ cost / Σ tokens / Σ time, NOT the last run alone."""
tid = _mk_task(stage="review") # past development -> ✅ shown
_mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00")
_mk_run(tid, "architect", "2026-06-04 09:10:00", "2026-06-04 09:20:00")
# Three developer attempts: $1.50 + $2.00 + $0.48 = $3.98; 30m total.
_mk_run(tid, "developer", "2026-06-04 09:20:00", "2026-06-04 09:30:00",
cost=1.50, in_tok=100, out_tok=40, cache_read=10)
_mk_run(tid, "developer", "2026-06-04 09:30:00", "2026-06-04 09:45:00",
cost=2.00, in_tok=200, out_tok=60, cache_creation=20)
_mk_run(tid, "developer", "2026-06-04 09:45:00", "2026-06-04 09:50:00",
cost=0.48, in_tok=50, out_tok=10)
text = N.render_task_tracker(tid)
line = _stage_line(text, "Разработка")
assert line is not None, text
# Σ cost = $3.98 (not the last $0.48).
assert fmt_cost(3.98) in line, line
assert fmt_cost(0.48) not in line, line
# Σ output tokens = 40+60+10 = 110.
assert f"{fmt_tokens(110)}" in line, line
# Σ input (input+cache_read+cache_creation): (100+10)+(200+20)+50 = 380.
exp_in = _input_total({"input_tokens": 100, "cache_read_tokens": 10,
"cache_creation_tokens": 0}) \
+ _input_total({"input_tokens": 200, "cache_read_tokens": 0,
"cache_creation_tokens": 20}) \
+ _input_total({"input_tokens": 50, "cache_read_tokens": 0,
"cache_creation_tokens": 0})
assert f"{fmt_tokens(exp_in)}" in line, line
# Σ time = 10+15+5 = 30m.
assert " 30м " in line, line
# =========================================================================== #
# TC-07 / AC-5 — task totals converge with SUM(agent_runs)
# =========================================================================== #
def test_tc07_totals_converge_with_sum_agent_runs():
"""The 💰 totals line equals SUM(agent_runs) over cost & tokens even with
retries (the stage lines and the totals draw from the same row set)."""
tid = _mk_task(stage="review")
_mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
cost=0.20, in_tok=30, out_tok=10)
_mk_run(tid, "architect", "2026-06-04 09:10:00", "2026-06-04 09:20:00",
cost=0.30, in_tok=40, out_tok=12)
_mk_run(tid, "developer", "2026-06-04 09:20:00", "2026-06-04 09:30:00",
cost=1.50, in_tok=100, out_tok=40, cache_read=10)
_mk_run(tid, "developer", "2026-06-04 09:30:00", "2026-06-04 09:45:00",
cost=2.00, in_tok=200, out_tok=60, cache_creation=20)
# Authoritative SUM straight from the DB.
conn = get_db()
rows = conn.execute(
"SELECT input_tokens, output_tokens, cache_read_tokens, "
"cache_creation_tokens, cost_usd FROM agent_runs WHERE task_id=?",
(tid,),
).fetchall()
conn.close()
sum_cost = sum(float(r["cost_usd"] or 0) for r in rows)
sum_out = sum(int(r["output_tokens"] or 0) for r in rows)
sum_in = sum(_input_total({"input_tokens": r["input_tokens"],
"cache_read_tokens": r["cache_read_tokens"],
"cache_creation_tokens": r["cache_creation_tokens"]})
for r in rows)
text = N.render_task_tracker(tid)
totals = [ln for ln in text.splitlines() if ln.startswith("💰")][0]
assert fmt_cost(sum_cost) in totals, totals
assert f"{fmt_tokens(sum_in)}" in totals, totals
assert f"{fmt_tokens(sum_out)}" in totals, totals
def test_tc07_sum_of_stage_lines_equals_totals_on_done():
"""On a done task with retries, Σ(stage-line costs) == totals cost: each agent
maps to exactly one stage row, so no run is double-counted or dropped."""
tid = _mk_task(stage="done")
_mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00", cost=0.20)
_mk_run(tid, "developer", "2026-06-04 09:20:00", "2026-06-04 09:30:00", cost=1.50)
_mk_run(tid, "developer", "2026-06-04 09:30:00", "2026-06-04 09:45:00", cost=2.00)
_mk_run(tid, "deployer", "2026-06-04 10:00:00", "2026-06-04 10:10:00", cost=0.30)
text = N.render_task_tracker(tid)
totals = [ln for ln in text.splitlines() if ln.startswith("💰")][0]
# developer stage line = Σ $3.50 (not $2.00), totals = $4.00.
dev_line = _stage_line(text, "Разработка")
assert fmt_cost(3.50) in dev_line, dev_line
assert fmt_cost(4.00) in totals, totals
# =========================================================================== #
# TC-08 / AC-7 — render_task_tracker never raises on broken/partial rows
# =========================================================================== #
def test_tc08_render_survives_null_timestamps_and_runs():
"""NULL timestamps / partial runs -> render returns a string, never raises."""
tid = _mk_task(stage="development")
# Run with NULL started/finished and NULL token columns.
conn = get_db()
conn.execute(
"INSERT INTO agent_runs (task_id, agent, started_at, finished_at, "
"exit_code, input_tokens, output_tokens, cost_usd, model) "
"VALUES (?,?,?,?,?,?,?,?,?)",
(tid, "developer", None, None, None, None, None, None, None),
)
conn.commit()
conn.close()
text = N.render_task_tracker(tid) # must not raise
assert isinstance(text, str) and text
def test_tc08_render_survives_bogus_stage():
"""A task sitting on a truly unknown stage still renders (never-raise)."""
tid = _mk_task(stage="__bogus__")
_mk_run(tid, "developer", "2026-06-04 09:00:00", "2026-06-04 09:10:00")
text = N.render_task_tracker(tid)
assert isinstance(text, str) and text
# Unknown stage -> developer's finished run is past "far future" current pos?
# current_pos for unknown = len(order) -> every real stage_key <= it -> ✅ kept
# (degrades to pre-ORCH-091 behaviour, no spurious suppression).
assert _stage_line(text, "Разработка") is not None, text

View File

@@ -110,9 +110,12 @@ def test_tc06_stage_to_plane_status(stage, expected):
assert N.plane_status_label({"stage": stage}) == expected
def test_tc06_unknown_stage_degrades_to_default():
# Anything unknown -> the safe stage default (To Analyse), never an error.
assert N.plane_status_label({"stage": "weird-stage"}) == "To Analyse"
def test_tc06_unknown_stage_degrades_to_neutral():
# ORCH-091 (AC-3): a genuinely unknown stage degrades to a NEUTRAL capitalized
# label, NOT the misleading "To Analyse". A broken row with no stage key falls
# back to 'created' -> "To Analyse" (the real first status), never an error.
assert N.plane_status_label({"stage": "weird-stage"}) == "Weird Stage"
assert N.plane_status_label({"stage": "weird-stage"}) != "To Analyse"
assert N.plane_status_label({}) == "To Analyse"
@@ -214,3 +217,68 @@ def test_tc09c_plane_status_label_never_raises():
# Garbage row (None / object without keys) -> safe default, no exception.
assert N.plane_status_label(None) == "To Analyse"
assert N.plane_status_label(object()) == "To Analyse"
# =========================================================================== #
# ORCH-091 — Group 1 (D1): completeness of the status map, staging label,
# neutral fallback. Plane_status_label is pure/offline -> assert directly.
# =========================================================================== #
from src.stages import STAGE_TRANSITIONS # noqa: E402
# --------------------------------------------------------------------------- #
# ORCH-091 TC-01 / AC-1 — completeness of the map vs STAGE_TRANSITIONS
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("stage", [s for s in STAGE_TRANSITIONS if s != "created"])
def test_orch091_tc01_every_stage_has_meaningful_label(stage):
"""AC-1: for EVERY STAGE_TRANSITIONS key (bar 'created') plane_status_label
returns a non-empty label that is NOT the misleading 'To Analyse'. Completeness
is derived programmatically from STAGE_TRANSITIONS (the single source of truth),
NOT a hardcoded list — a new engine stage without a curated label fails here."""
label = N.plane_status_label({"stage": stage})
assert label, f"stage {stage!r} produced an empty label"
assert label != N._DEFAULT_STATUS_LABEL, (
f"stage {stage!r} still falls back to 'To Analyse'"
)
# The curated map must actually carry the key (not just a neutral autogen).
assert stage in N._STAGE_STATUS_LABEL, (
f"stage {stage!r} missing a curated label in _STAGE_STATUS_LABEL"
)
def test_orch091_tc01_created_stays_to_analyse():
# 'created' keeps the meaningful real first status.
assert N.plane_status_label({"stage": "created"}) == "To Analyse"
# --------------------------------------------------------------------------- #
# ORCH-091 TC-02 / AC-2 — staging label is meaningful and distinct
# --------------------------------------------------------------------------- #
def test_orch091_tc02_deploy_staging_label():
"""AC-2: stage='deploy-staging' -> a meaningful staging label, distinct from
'To Analyse' AND from the deploy stage's Awaiting-Deploy label."""
staging = N.plane_status_label({"stage": "deploy-staging"})
deploy = N.plane_status_label({"stage": "deploy"})
assert staging == "Deploying (staging)"
assert staging != "To Analyse"
assert staging != deploy
assert "staging" in staging.lower()
# --------------------------------------------------------------------------- #
# ORCH-091 TC-03 / AC-3 — neutral fallback for a truly unknown stage
# --------------------------------------------------------------------------- #
def test_orch091_tc03_unknown_stage_neutral_not_to_analyse():
"""AC-3: a genuinely unknown stage -> neutral capitalized label (NOT
'To Analyse'); never raises on broken/None input."""
assert N.plane_status_label({"stage": "__bogus__"}) != "To Analyse"
assert N.plane_status_label({"stage": "__bogus__"}) # non-empty
# never-raise on broken input; None/missing-key degrade to the safe default.
assert N.plane_status_label(None) == "To Analyse"
assert N.plane_status_label({"stage": None}) == "To Analyse"
assert N.plane_status_label({"stage": ""}) == "To Analyse"
def test_orch091_tc03_cancelled_offline_label():
# ORCH-090 terminal: offline base label, no longer 'To Analyse'.
assert N.plane_status_label({"stage": "cancelled"}) == "Cancelled"