Files
orchestrator/docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md

21 KiB
Raw Blame History

ADR-001: Осмысленная статусная модель Plane (слой B)

Work Item: ORCH-066 Стадия: architecture Автор: Architect Дата: 2026-06-07 Статус: Accepted

Контракт резолвера, алиасинга и разводки точек простановки статуса. Опирается на BRD (01-brd.md), ТЗ (02-trz.md), критерии приёмки (03-acceptance-criteria.md). Инфра-предусловие (статусы, создаваемые оператором) — 07-infra-requirements.md, риски — 10-tech-risks.md.


1. Контекст

Plane-доска оркестратора семантически перегружена: In Progress одновременно означает «человек запускает конвейер», «идёт анализ», «идёт прод-деплой» и «возврат из Needs Input». Оператор не различает реальный этап задачи → риск ошибочного ручного перевода статуса. ORCH-059 уже разгрузил Approved отдельным Confirm Deploy; ORCH-066 завершает наведение порядка по утверждённой Owner модели.

Жёсткое разделение двух слоёв (инвариант проекта):

Слой Что Источник ORCH-066
A STAGE_TRANSITIONS — машина стадий src/stages.py НЕ трогаем
B Plane-статусы — индикация на доске src/plane_sync.py + точки простановки меняем только это

Статус — индикация, не управление. Машинные вердикты по-прежнему читаются только из YAML-frontmatter артефактов (канон гейтов). Конвейер движут гейты слоя A; смена Plane-статуса не может продвинуть/откатить задачу (кроме существующих человеческих триггеров To Analyse/Approved/Rejected, которые и раньше были входами).

Целевая модель Owner:

Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
Monitoring after Deploy → Done

[...] = действие человека (вход-триггер); остальное ставит орк (индикация). Ветки: Rejected (откат), Needs Input (только аналитик), Blocked (затык/фейл деплоя/деградация), Cancelled (человек отменил задачу).


2. Решение

2.1. Реестр логических статусов (src/plane_sync.py)

Вводим 6 новых логических ключей. Имена в _PLANE_NAME_TO_KEY (резолв по имени из Plane API):

Логический ключ Plane name Назначение
to_analyse To Analyse Вход-триггер: старт нового конвейера и resume аналитика из Needs Input.
analysis Analysis Индикация стадии analysis (орк).
code_review Code-Review Индикация стадии review (орк). Заменяет review как видимый статус.
awaiting_deploy Awaiting Deploy Phase A approval-pending (орк).
deploying Deploying Phase B прод-деплой идёт (орк).
monitoring Monitoring after Deploy Phase C / post-deploy окно (орк).

Существующие ключи сохраняются: backlog, todo, in_progress, needs_input, in_review, blocked, done, cancelled, architecture, development, review, testing, approved, rejected. Cancelled уже присутствует.

2.2. Fail-closed резолюция — project-relative alias-fallback (КРИТИЧНО, BR-12)

ТЗ §2.2 предложил статические алиасы на enduro-UUID в _DEFAULT_STATES. Архитектурное уточнение: для частично сконфигурированного проекта (оператор создал не все новые статусы) статический enduro-UUID в orchestrator-проекте даст невалидный state → PATCH 422/404. Поэтому деградация делается относительно того же проекта, а не на чужой UUID.

Два уровня fallback в get_project_states() (success-path), строго в порядке:

  1. Резолв по имени из Plane API (как сейчас).

  2. Alias-fallback (новый): для каждого отсутствующего нового ключа — UUID его базового ключа из этого же проекта:

    _STATE_ALIAS_FALLBACK = {
        "to_analyse":      "in_progress",
        "analysis":        "in_progress",
        "code_review":     "review",
        "awaiting_deploy": "in_review",
        "deploying":       "in_progress",
        "monitoring":      "done",
    }
    # после резолва по имени, ДО _DEFAULT_STATES.setdefault:
    for new_key, base_key in _STATE_ALIAS_FALLBACK.items():
        if new_key not in resolved and resolved.get(base_key):
            resolved[new_key] = resolved[base_key]
    
  3. _DEFAULT_STATES.setdefault(...) (как сейчас) — последний резерв для путей, где API недоступен целиком (if not project_id: return _DEFAULT_STATES, полный провал запроса). В _DEFAULT_STATES новые ключи ТОЖЕ добавляются (= enduro-UUID базового ключа), чтобы любой caller всегда получал полный словарь и states[key] не кидал KeyError.

Эффект деградации:

Сценарий Поведение
Orchestrator: все новые статусы созданы резолв по имени → новые UUID (целевая модель).
Orchestrator: создана ЧАСТЬ новых статусов отсутствующие → собственный базовый UUID проекта → индикация деградирует до текущего статуса, PATCH валиден.
Enduro (новые статусы не создаются никогда) alias-fallback → собственные enduro базовые UUID → строго прежнее поведение (In Progress/Review/Done).
Plane API down целиком _DEFAULT_STATES (enduro-UUID) — без регресса относительно сегодняшнего поведения.

Это паттерн ORCH-059 AC-7, усиленный project-relative разрешением. Все set_issue_* и _set_issue_state_direct остаются never-raise (PATCH-исключение логируется, не пробрасывается) — индикация деградирует, слой A не затрагивается.

2.3. Маппинг стадия → статус

  • _STAGE_TO_STATE_KEY (живой путь update_issue_statestage_to_state): analysisanalysis (было in_progress); reviewcode_review (было review). deploy остаётся in_progress (управляется Phase A/B/C напрямую). Остальные — без изменений.
  • STAGE_VISIBILITY_STATE: reviewcode_review; добавить analysisanalysis (для консистентности; set_issue_stage_state сейчас dormant, но карта обновляется).
  • STAGE_TO_STATE (legacy/test-only) — обновить analysis_DEFAULT_STATES["analysis"], review_DEFAULT_STATES["code_review"]. UUID-значения байт-в-байт прежние (это алиасы на те же in_progress/review UUID) → тесты на конкретные UUID не краснеют.

2.4. Новые хелперы src/plane_sync.py

Тонкие обёртки по образцу set_issue_in_review (per-project резолв + _set_issue_state_direct, never-raise):

  • set_issue_analysis(work_item_id, project_id=None)
  • set_issue_code_review(work_item_id, project_id=None)
  • set_issue_awaiting_deploy(work_item_id, project_id=None)
  • set_issue_deploying(work_item_id, project_id=None)
  • set_issue_monitoring(work_item_id, project_id=None)

get_project_states всегда возвращает полный словарь (см. §2.2), поэтому [key] не кидает KeyError.

2.5. Точки простановки статуса (разводка)

Файл:место Сейчас Должно стать AC
webhooks/plane.py handle_issue_updated new_state == in_progresshandle_status_start new_state == to_analysehandle_status_start (при алиасе совпадает с in_progress) AC-1, AC-17
webhooks/plane.py start_pipeline (успешный старт) статус остаётся In Progress в конце старта орк ставит set_issue_analysis AC-3
webhooks/plane.py handle_status_start (resume-ветка) relaunch агента стадии при relaunch орк ставит set_issue_analysis; fork «старт vs resume» (get_task_by_plane_id + has_active_job_for_task) — без изменений AC-2, AC-4
webhooks/plane.py _rollback_stage (reject@analysis, ~583) set_issue_in_progress set_issue_analysis AC-3
stage_engine.py _handle_analysis_approved_flow (artifacts ready) set_issue_in_review без изменений (BR-9) AC-13
stage_engine.py _handle_analysis_approved_flow (questions) set_issue_needs_input без изменений (BR-10) AC-14
stage_engine.py rollback@analysis (architect conflict, ~669) set_issue_in_progress set_issue_analysis AC-3
stage_engine.py _handle_self_deploy_phase_a (~1012) set_issue_in_review set_issue_awaiting_deploy AC-6, AC-13
stage_engine.py _handle_self_deploy_phase_b (после INITIATED marker) статус не меняет set_issue_deploying AC-7
stage_engine.py terminal-sync deploy → done (~338) set_issue_done для всех self (post_deploy_applies): set_issue_monitoring; не-self: set_issue_done как сейчас AC-8, AC-9
stage_engine.py run_post_deploy_monitor HEALTHY+окно закрыто (~1260) статус не трогает set_issue_done (явно) AC-10
stage_engine.py run_post_deploy_monitor DEGRADED (~1273) alert/log set_issue_blocked (+ существующий ALERT_ONLY) AC-11

Разводка terminal-sync (детально, AC-8/AC-9). Текущий код безусловно зовёт set_issue_done на next_stage == "done", затем (для self) армит post-deploy monitor. Разводим по post_deploy.post_deploy_applies(repo):

if next_stage == "done" and work_item_id:
    if post_deploy.post_deploy_applies(repo):
        set_issue_monitoring(work_item_id)   # self: окно наблюдения, НЕ Done сразу
    else:
        set_issue_done(work_item_id)          # не-self: терминальный Done как сейчас
# арм монитора (существующий блок ~361) — без изменений

Финальный Done/Blocked для self-hosting перекладывается на run_post_deploy_monitor. При деградированном алиасе monitoring==done self-hosting показывает Done и затем монитор держит Done/флипает Blocked — поведение идентично сегодняшнему.

AC-12 (инвариант ORCH-021): добавление set_issue_blocked в DEGRADED-ветку — только индикация; тик по-прежнему НИКОГДА не рестартит/откатывает прод-контейнер (self-hosting остаётся ALERT_ONLY). set_issue_blocked — Plane-PATCH, не действие над контейнером.

Cancelled (AC-15): изменений кода НЕ требует. handle_issue_updated реагирует только на to_analyse/approved/rejected; Cancelled падает в else → «no pipeline action». Орк не делает advance/rollback — индикация, не управление. Критерий выполнен существующим кодом.

2.6. Reconciler (src/reconciler.py)

  • F-2 _reconcile_plane_project: заменить триггер in_progressto_analyse в списке запрашиваемых статусов (list_issues_by_state([to_analyse, approved, rejected])) и в _reconcile_plane_issue маршрутизировать new_state == to_analysehandle_status_start. При алиасе to_analyse == in_progress (enduro) поведение идентично текущему (один UUID; list_issues_by_state дедуплицирует через set). AC-19.

  • Guard 2 _is_blocked_or_needs_input: расширить skip-множество активными ожиданиями awaiting_deploy/deploying/monitoring (BR-13, AC-20). Анти-регресс enduro (КРИТИЧНО): новые ключи алиасятся на in_review/in_progress/done; добавить их в skip «как есть» → на enduro In Progress/Done-задачи начнут ошибочно пропускаться F-1 (регресс ORCH-053/060). Поэтому активные ожидания включаются в skip только когда они РАЗЛИЧНЫ от базовых рабочих статусов проекта (т.е. реально созданы):

    base_working = {states.get(k) for k in (
        "backlog","todo","in_progress","in_review","review",
        "architecture","development","testing","approved","rejected","done")}
    extra_waits = {states.get("awaiting_deploy"),
                   states.get("deploying"),
                   states.get("monitoring")} - base_working - {None}
    skip_set = {states.get("blocked"), states.get("needs_input")} | extra_waits
    return cur in skip_set
    

    Enduro (алиасы схлопываются в base) → extra_waits == {} → нулевой регресс. Orchestrator (отдельные UUID) → три реальных статуса в skip → BR-13. Семантику метода обобщаем до «human-or-active-wait»; флаг reconcile_skip_blocked_enabled продолжает гасить этот networked-чек. F-1 и так структурно не оживляет эти состояния (Phase A: check_deploy_status red → silent; Deploying: active finalizer job → active-job guard; Monitoring: стадия done → не итерируется) — Guard 2 это defense-in-depth по требованию Owner.

2.7. Без kill-switch

Отдельный env-флаг новой модели не вводится. Раскат естественно гейтится инфра-предусловием: пока оператор не создал новые статусы — alias-fallback (§2.2) держит строго прежнее поведение; создал — резолв по имени включает новую модель. Это проще отдельного флага и соответствует принципу «минимум зависимостей». (ТЗ §6 допускает флаг как опциональный — сознательно отказываемся.)


3. Затронутые модули (карта изменений)

Модуль Изменение
src/plane_sync.py _PLANE_NAME_TO_KEY +6; _DEFAULT_STATES +6 (enduro-alias UUID); _STATE_ALIAS_FALLBACK (новое) + применение в get_project_states; _STAGE_TO_STATE_KEY (analysis/review); STAGE_VISIBILITY_STATE; STAGE_TO_STATE (legacy); 5 новых set_issue_*.
src/webhooks/plane.py триггер in_progressto_analyse в handle_issue_updated; set_issue_analysis в start_pipeline и resume-ветке handle_status_start; _rollback_stage reject@analysis → set_issue_analysis.
src/stage_engine.py Phase A → set_issue_awaiting_deploy; Phase B → set_issue_deploying; terminal-sync split (monitoring vs done); post-deploy monitor HEALTHY→set_issue_done, DEGRADED→set_issue_blocked; rollback@analysis (architect conflict) set_issue_in_progressset_issue_analysis.
src/reconciler.py F-2 триггер to_analyse; Guard 2 skip-set + анти-регресс subtraction.
src/stages.py НЕ трогаем (инвариант слоя A).
src/config.py Без изменений (kill-switch не вводится).

4. Инварианты (проверяемые, AC-21/AC-22)

  • src/stages.py STAGE_TRANSITIONS — diff пуст (байт-в-байт).
  • QG_CHECKS, check_deploy_status/_parse_deploy_status, exit-коды хука (0/1/2), merge-gate, check_branch_mergeable/check_staging_image_fresh, схема БД — без изменений.
  • Confirm Deploy (ORCH-059), механизм Needs Input (analyst-only) — без изменений.
  • Новых HTTP-эндпоинтов нет; GET /queue/GET /status контракт без изменений.
  • Миграций БД нет (tasks не хранит Plane-статус; источник истины — стадия в БД + Plane API).
  • Все новые set_issue_* / резолв — never-raise.
  • Не-self (enduro) терминальный deploy → Done — без регресса.

5. Последствия

Плюсы

  • Доска читаема: каждый этап = осмысленный статус; человеческие входы визуально отделены от индикации.
  • In Progress разгружен: больше не «всё подряд».
  • Fail-closed усилен (project-relative): частичная конфигурация не ломает ни индикацию, ни конвейер.
  • Слой A нетронут → нулевой риск для машины стадий и гейтов всех проектов (self-hosting).
  • Нет нового флага/таблицы → меньше движущихся частей.

Минусы / ограничения

  • Требуется ручное инфра-действие оператора (создать 6 статусов в проекте ORCH) — до этого orchestrator деградирует до старой индикации (см. 07-infra-requirements.md).
  • Статусы кэшируются per-process (_STATES_CACHE): после создания статусов нужен reload_project_states() или рестарт staging (не прод — см. self-hosting риск).
  • Guard-2 subtraction добавляет немного логики; покрывается тестами (enduro-алиас → пустой extra; orchestrator → три статуса).

Self-hosting (⚠️): изменения — слой B (Plane-индикация) + reconciler-гварды; машина стадий и контракты деплоя нетронуты. Выкладка ОБЯЗАТЕЛЬНО через deploy-staging (8501) до прод-деплоя орка. Прод-контейнер не рестартить в рамках задачи вне штатного staging-гейта.


6. Альтернативы (отклонены)

  • Статический enduro-UUID алиас (ТЗ §2.2 буквально): ломается на частичной конфигурации orchestrator-проекта (чужой UUID → PATCH 422). Заменён project-relative alias-fallback (§2.2).
  • Глобальный env kill-switch новой модели: избыточен — инфра-предусловие уже даёт естественный гейт раската (§2.7).
  • Хранить Plane-статус в tasks (миграция БД): не нужно; источник истины — стадия + живой Plane API. Нарушило бы инвариант «без лишних зависимостей».
  • Менять STAGE_TRANSITIONS ради новых статусов: запрещено (инвариант слоя A); статусы — индикация, отделены от машины стадий.