architect(ET): auto-commit from architect run_id=733
All checks were successful
CI / test (push) Successful in 1m9s

This commit is contained in:
2026-06-16 01:37:27 +03:00
parent ac203c0ccf
commit f120e4bd8f
7 changed files with 596 additions and 0 deletions

View File

@@ -10,6 +10,7 @@
- **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`.
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`. **ORCH-109 ([adr-0040](adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md)):** (1) резолвенная **модель стампится в `agent_runs.model` в момент launch** (`_spawn`, объединённый `UPDATE … SET model=?, effort=?` рядом со стампом эффорта ORCH-087; пустой резолв → `NULL`; never-raise) → модель видна не-`null` при любом исходе прогона, включая timeout-kill (`exit_code=-9`), и in-flight в `GET /metrics`/`GET /queue` (`get_running_agents` уже отдаёт `model`); постфактум `record_usage` (`model=COALESCE(?, model)`) остаётся **обогащением**, не единственным источником истины. (2) **Per-role wall-clock бюджеты** через выделенные ключи `agent_timeout_developer_s=3600`/`agent_timeout_reviewer_s=3000` (лестница `_resolve_timeout`: `agent_timeout_overrides_json` → выделенный ключ роли → `agent_timeout_seconds=1800`; прочие роли — байт-в-байт; малформный/вне-диапазонный конфиг → дефолт + WARNING). Инвариант reaper ORCH-065 сохранён синхронным поднятием `reaper_max_running_s` 3600→**5400** (`5400 > max(timeout)3600 + grace20`). FR-5 анти-salvage — структурно: продвижение гейтится `if exit_code==0`, timeout-kill → `_finalize_job` (retry/fail), не advance. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты.
- **Staging-runner** (`src/staging_runner.py`, ORCH-115 — [adr-0048](adr/adr-0048-deterministic-staging-runner.md)) — чистый **never-raise** leaf (паттерн `self_deploy`/`proc_group`/`staging_verdict`), заменяющий **LLM-агента `deployer` на стадии `deploy-staging`** детерминированным кодом (первый реализованный срез determinization-roadmap, A6/`replace-deterministic-now`). Перехват в `launch_job` **до `_spawn`** (рядом с D1/D2 `deploy-finalizer`/`post-deploy-monitor`): дискриминатор — **стадия задачи** `deploy-staging` **И** `applies(repo)` (kill-switch `staging_runner_enabled` + скоуп `staging_runner_repos`, пусто → self-hosting only), `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM). Раннер (зеркало `run_deploy_finalizer`) исполняет ту же staging-сюиту (`docker exec orchestrator-staging … staging_check.py`) через `proc_group` (tree-kill, таймаут `staging_runner_timeout_s=600`), маппит exit-код единым контрактом `self_deploy.map_exit_code_to_status` (`0→SUCCESS`/иначе→`FAILED`; ORCH-061 infra-tolerance остаётся внутри `staging_check.py`), пишет `15-staging-log.md` (тот же machine-key `staging_status:`, `author_agent: staging-runner`/`model_used: n/a`) + best-effort push в фичеветку, и вызывает **существующий** `advance_stage(current_stage="deploy-staging", finished_agent="deployer")` — без новых рёбер/исходов; transition-lease ORCH-114 берётся внутри `advance_stage` (раннер не трогает). **Двухуровневый исход (анти-ORCH-110):** сюита исполнилась → verdict→advance (FAILED → тот же откат `deploy-staging → development` + developer-retry, что у LLM); tool-error/таймаут → bounded defer (`staging_runner_infra_max_retries`) → fail-closed `FAILED` + alert на исчерпании (инфра ≠ код-фейл, никогда тихий advance/ложный green). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_staging_status`/`_parse_staging_status`/machine-verdict/схема БД — **байт-в-байт не тронуты** (замена *продюсера*, не гейта). Наблюдаемость — in-process счётчики + блок `staging_runner` в `GET /queue`. Откат — `ORCH_STAGING_RUNNER_ENABLED=false` (прежний LLM-deployer-путь байт-в-байт). Детали — `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`.
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`.
- **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts<max``queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`). **ORCH-113 ([adr-0043](adr/adr-0043-reaper-finalizer-liveness-ownership.md)):** на ребре `deploy-staging → deploy` тяжёлые edge-под-гейты (security/merge-gate re-test/coverage/image-freshness) исполняются в потоке монитора **после** штампа `finished_at` и **до** `_finalize_job` — минуты, а Tier-2 `finished_age_s` меряется от `finished_at`, поэтому живой долго финализирующий монитор ошибочно реапился (инцидент ORCH-111: повторный re-test → ложный откат `deploy-staging → development` параллельно успешному deploy). Фикс — процесс-локальный реестр владения финализацией (leaf `src/finalizer_liveness.py`, never-raise): монитор `mark()`/`clear()` (try/finally), reaper в Tier-2 при `stage=="deploy-staging"` И активном владении делает **defer** (не повторяет advance); Tier-3 backstop маркер игнорирует (мёртвый/застрявший finalizer добивается в ограниченное время). In-memory restart-safe через `requeue_running_jobs` (вызов до старта reaper); схема БД, `reaper_finalize_grace_s`/`reaper_max_running_s` и сквозной бюджет не тронуты. Kill-switch `ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`.
- **Transition-ownership lease** (`src/transition_lease.py` + таблица `transition_lease`, ORCH-114 — реализовано, [adr-0045](adr/adr-0045-transition-ownership-lease-and-stage-cas.md)) — чистый **never-raise** leaf (паттерн `serial_gate`/`coverage_gate`/`finalizer_liveness`; импортирует только `db`+`config`, лениво `merge_gate.pid_alive`/`qg.checks`/`notifications`; НЕ импортирует `stage_engine`/`launcher`), закрывающий корневой класс инцидент-цепочки ORCH-110/111/112/113 — у side-effectful переходов стадий не было единого владения. Два слоя, оба под единым kill-switch: **(1) durable transition-lease** (владение на ВХОДЕ в side-effectful регион — аддитивная таблица `transition_lease`, `task_id PK`; `acquire` атомарным rowcount-guard `INSERT … ON CONFLICT DO NOTHING` после очистки stale-строки; liveness владельца = `owner_pid`+`owner_boot_id`, НЕ heartbeat → рестарт-recovery бесплатен) и **(2) expected-stage CAS** `db.update_task_stage_cas` (на ЗАПИСИ стадии; проигравший гонку — аборт без побочных эффектов; покрывает и 6 путей записи в обход `advance_stage`). `advance_stage` захватывает lease на side-effectful рёбрах (`deploy-staging`/`deploy`) и освобождает в `try/finally`; job-reaper `_finalizer_owns` обобщён до durable cross-path lease (defer при живом, реклейм мёртвого; Tier-3 backstop игнорирует маркер → bounded; реап force-освобождает lease); reconciler F-1 и Plane-webhook делают defer; `main.lifespan` зовёт `recover_on_startup()` после `requeue_running_jobs`. Lease без своего TTL (потолок = Tier-3 `reaper_max_running_s`) → сквозной бюджет ORCH-065/109/110/113 цел. Скоуп self-hosting (`transition_lease_repos=""` → только `orchestrator`); kill-switch `ORCH_TRANSITION_LEASE_ENABLED=false` → CAS вырождается в прежний `update_task_stage`, lease инертен, reaper → ORCH-113 fallback (байт-в-байт). Наблюдаемость — блок `transition_lease` в `GET /queue` + Telegram-алерт на форсированный/устаревший реклейм + опц. `POST /transition-lease/release?work_item=<id>`. `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схемы существующих таблиц — байт-в-байт. Подробнее ниже (§ «Единое владение side-effectful переходами»). Детали — `docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`.
@@ -441,6 +442,12 @@ ORCH-079 синхронизирует витрину с кодом и закры
control-path-инвариант сверки с `src/qg/checks.py` и фиксацию avoidable-набора).
- **Норматив сопровождения:** менял места вызова LLM **или** потребителя вердикта в `src/qg/checks.py`
→ обнови карту/разметку и политику в том же PR.
- **Первый срез реализован (ORCH-115 — [adr-0048](adr/adr-0048-deterministic-staging-runner.md)):**
rank-1 кандидат **deployer (staging-status)** заменён детерминированным `src/staging_runner.py`
(перехват в `launch_job` до `_spawn`, как D1/D2) — A6 переходит из «план» в «реализовано». Карта
`llm-call-sites.md` (A6), `llm-determinization-roadmap.md` (rank 1, инвариант «ровно один
`first_slice`») и анти-дрейф-тесты обновляются **атомарно с кодом** в development (норматив
сопровождения NFR-6). Второй кандидат — `tester`-гибрид (rank 2) — остаётся планом.
- ADR: [adr-0047](adr/adr-0047-llm-usage-policy-and-call-site-map.md); детально —
`docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`.

View File

@@ -0,0 +1,92 @@
---
work_item: ORCH-115
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-16
model_used: claude-opus-4-8
---
# adr-0048: Детерминированный staging-раннер — первый реализованный срез determinization-roadmap
> **Сквозной (cross-cutting) ADR.** Агрегирует решение ORCH-115, влияющее на **весь**
> оркестратор: вводит новый компонент-leaf `src/staging_runner.py`, снимает первую
> avoidable LLM-консультацию (`deployer`/`staging-status`, A6) и переводит rank-1
> determinization-roadmap из «план» в «реализовано». Локальная детализация (все решения
> D1D11) — `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`.
## Статус
Proposed
## Контекст
ORCH-118 ([adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)) зафиксировал
нормативную политику и карту LLM-консультаций и назвал **avoidable LLM control paths =
`{tester, deployer}`**, поставив **deployer (staging-status)** первым срезом
(`first_slice = yes`, `replace-deterministic-now`, `hybrid_needed = no`). ORCH-118 раннеры
**не реализовывал** (docs+tests). ORCH-115 — первая фактическая реализация этого среза.
Вердикт `staging_status:` на стадии `deploy-staging` сейчас эмитит LLM-агент `deployer`, но
он есть **чистый маппинг exit-кода** `scripts/staging_check.py` (infra-tolerance ORCH-061
уже внутри скрипта), а гейт `check_staging_status` детерминирован. Это удовлетворяет обоим
условиям «avoidable»: C-консультация **и** деривируемый вердикт. Прецедент детерминированной
замены агента (`launch_job`-перехват до `_spawn`, D1/D2 `deploy-finalizer`/`post-deploy-monitor`)
и эталон «детерминированный джоб → `advance_stage`» (`run_deploy_finalizer`) уже работают в
проде — архитектурный риск снят.
## Решение
**Новый leaf `src/staging_runner.py` + перехват в `launch_job` до `_spawn`** (рядом с D1/D2).
На `deploy-staging` для in-scope репо джоб `deployer` обрабатывает раннер: исполняет
staging-сюиту через `proc_group` (tree-kill, ORCH-110), маппит exit-код единым контрактом
`self_deploy.map_exit_code_to_status`, пишет `15-staging-log.md` (тот же machine-key
`staging_status:`), вызывает **существующий** `advance_stage(finished_agent="deployer")`.
Кросс-каттинговые инварианты (сохранены **байт-в-байт**):
- `STAGE_TRANSITIONS` (`src/stages.py`), реестр и имена `QG_CHECKS`/`check_*`/`_parse_*`,
machine-verdict-ключи (`staging_status:`/`deploy_status:`/`verdict:`/`result:`/
`security_status:`/`coverage_status:`), **схема БД** — не тронуты. Это замена *продюсера*
артефакта, не гейта/стадии.
- Единственный транспорт LLM-консультации (`launcher._spawn`/S0,
[llm-usage-policy.md](../llm-usage-policy.md) §5) — соблюдён: раннер **не зовёт LLM**;
второй транспорт не вводится.
- Сквозной бюджет времени ORCH-065/109/110 (`reaper_max_running_s` > Σ(работ на ребре
`deploy-staging`) + grace) — соблюдён **без** правки `reaper_max_running_s` (раннер-таймаут
600s ≤ прежнего LLM-окна).
- Граница ORCH-112/ORCH-114: transition-lease берётся **внутри** `advance_stage`; раннер
lease/гигиену не модифицирует.
Скоуп — **self-hosting only** (`staging_runner_repos=""``is_self_hosting_repo`), под
kill-switch `staging_runner_enabled` (off → `_spawn` LLM-deployer'а байт-в-байт). never-raise
во всех публичных функциях; **двухуровневый исход** (verdict при исполнившейся сюите; bounded
defer → fail-closed на tool-error/таймауте) убирает с staging-ребра RCA-класс ORCH-110 (инфра
≠ код-фейл).
**Эволюция карты LLM (норматив сопровождения, в том же PR — D11 локального ADR):**
`llm-call-sites.md` (A6 → реализовано детерминированно), `llm-determinization-roadmap.md`
(rank 1 deployer → реализован; инвариант «ровно один `first_slice`» цел), `llm-usage-policy.md`
(§5 — транспорт не нарушен), плюс анти-дрейф-тесты (`test_llm_call_site_inventory.py`/
`test_llm_determinization_docs.py`). Эти правки коуплены к коду → применяются в development
атомарно с реализацией, не в architecture-стадии.
## Последствия
- **+** Минус один avoidable LLM control path; первый доказанный раннер-паттерн замены
C-консультации (опора для второго кандидата — `tester`-гибрид, rank 2).
- **+** Дешевле/быстрее/детерминированнее собственный `deploy-staging`; нет токенов/латентности
LLM в точке ветвления.
- **+** Паттерн переиспользуем: leaf + перехват до `_spawn` + `advance_stage` — шаблон для
будущих срезов и для Phase 2 (project deploy contract не-self репо).
- **** Новый компонент + врезка + defer-механика. Митигейшн: never-raise leaf, kill-switch
(fail-safe к LLM), без схемы БД, структурное покрытие.
- **Откат:** `ORCH_STAGING_RUNNER_ENABLED=false` → прежний LLM-путь на `deploy-staging`
байт-в-байт.
## Ссылки
- Локальный ADR: `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`
- Политика/карта/roadmap: [llm-usage-policy.md](../llm-usage-policy.md),
[llm-call-sites.md](../llm-call-sites.md), [llm-determinization-roadmap.md](../llm-determinization-roadmap.md),
[adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)
- Прецеденты: D1/D2 (`launcher.py:389/394`), `run_deploy_finalizer` (`stage_engine.py:2010`),
`proc_group` (ORCH-110, [adr-0042](adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md)),
transition-lease (ORCH-114, [adr-0045](adr-0045-transition-ownership-lease-and-stage-cas.md))

View File

@@ -96,6 +96,20 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
3. Стартует **watchdog thread** (per-role wall-clock бюджет → SIGTERM→grace→SIGKILL по pid; ORCH-109: developer 60 мин / reviewer 50 мин / прочие 30 мин дефолт, `_resolve_timeout`)
4. Стартует **monitor thread**: `proc.wait()` (гарантированный reap → реальный exit_code в БД) → закрывает log_fh → git commit/push → auto-advance
**Детерминированные перехваты `launch_job` ДО `_spawn` (no-LLM джобы).** Перед `_spawn` `launch_job`
перехватывает зарезервированные роли и исполняет их детерминированно, сам ведя `jobs`-строку через
`mark_job` (нет `agent_runs`, нет токенов): `deploy-finalizer` (D1, ORCH-036 Phase C) и
`post-deploy-monitor` (D2, ORCH-021). **ORCH-115 ([adr-0048](adr/adr-0048-deterministic-staging-runner.md)):**
тем же паттерном перехватывается джоб `deployer` на стадии `deploy-staging` для in-scope репо
(дискриминатор — **стадия задачи**, не имя роли: роль `deployer` общая для `deploy-staging`/`deploy`;
+`staging_runner.applies(repo)` под kill-switch `staging_runner_enabled`, скоуп `staging_runner_repos`,
пусто → self-hosting only; `should_intercept` never-raise → `False` → штатный `_spawn`, fail-safe к
LLM). Leaf `src/staging_runner.py` (зеркало `run_deploy_finalizer`) исполняет staging-сюиту через
`proc_group` (tree-kill, таймаут `staging_runner_timeout_s`), маппит exit-код
`self_deploy.map_exit_code_to_status`, пишет `15-staging-log.md` (тот же machine-key `staging_status:`)
и вызывает существующий `advance_stage(finished_agent="deployer")` (см. §5). Так LLM-агент `deployer`
на `deploy-staging` исчезает из happy-path; `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты.
### 5. Auto-advance (`launcher._try_advance_stage`)
После успешного завершения агента: