Compare commits
92 Commits
feature/OR
...
feature/OR
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41da03470a | ||
|
|
9979eec168 | ||
| c991b9de1a | |||
| 3d7d751b7a | |||
| f330a580c4 | |||
| 896ecf6acb | |||
| 096c452230 | |||
| 9f176036f1 | |||
| 3e4191050f | |||
| 38e329f6f7 | |||
| 58d6c433d1 | |||
| 52ca882e5b | |||
| d49e88cf3f | |||
| e7a5b50f97 | |||
| 034343ec5d | |||
| cc87beb2b4 | |||
| fb25e9a0cf | |||
| 2824fd8543 | |||
| c26a6b637c | |||
| dd5fe619d5 | |||
| f6b5671267 | |||
| 49461238f1 | |||
|
|
c90c01b919 | ||
|
|
2ec6873e33 | ||
|
|
cac6539698 | ||
|
|
af7472df05 | ||
|
|
995ba0af71 | ||
| 772ccab013 | |||
|
|
06271b0bfb | ||
| 101bd1c512 | |||
|
|
aa4161fc78 | ||
| 6bbd530caa | |||
| 4b03f213f7 | |||
| 1d72c44587 | |||
| 0605309602 | |||
| 044894cbe9 | |||
| cb11137a77 | |||
| 48b54051e5 | |||
|
|
72d662ae88 | ||
|
|
348cf8c164 | ||
| bc2347abd3 | |||
| 62c1fe3461 | |||
| 0dfddf93f0 | |||
| 22d3b77426 | |||
| 4a06537afd | |||
| b6c0e11e4d | |||
| 3fb3d15cb4 | |||
|
|
9f4d79baee | ||
|
|
7cdef6d377 | ||
|
|
0cbb7ef0bb | ||
| ca41d9210b | |||
| 48943fe10a | |||
| 86fe8dd509 | |||
| dd07b58165 | |||
| b67a61ecef | |||
| 8fcb867dcf | |||
| 4815e378d9 | |||
|
|
e07ee9e574 | ||
| 8cdb9f194a | |||
| cb3bdd9c7a | |||
|
|
04233cb3c8 | ||
|
|
85ecf50926 | ||
| 30b6187c73 | |||
| 44db94e462 | |||
| 4f24f96169 | |||
| 2d20da295e | |||
| 67e98b8296 | |||
|
|
cad5e98892 | ||
| bb03350ec9 | |||
| 930e65298c | |||
| cba67a4270 | |||
| 720c31393a | |||
| 9b7c855df3 | |||
| a6b444c356 | |||
| dbf14e3d5a | |||
| 4bebb921ff | |||
| 9f846b5a50 | |||
| b760b24a48 | |||
| f0ac9d5562 | |||
| 987ea810bf | |||
| f85e449d80 | |||
| 1c89ac9df9 | |||
| 03d899812c | |||
| b9bcdc1545 | |||
| b04fae748e | |||
| fbfcd84b16 | |||
| 2f4c553fd8 | |||
| 2bdba532d5 | |||
| db83b89467 | |||
| 961c5e9eee | |||
| 84a6f61ba8 | |||
| 1af356a343 |
107
.env.example
107
.env.example
@@ -12,11 +12,25 @@ ORCH_GITEA_WEBHOOK_SECRET=
|
||||
ORCH_CLAUDE_BIN=/usr/bin/claude
|
||||
ORCH_REPOS_DIR=/home/slin/repos
|
||||
ORCH_DB_PATH=/app/data/orchestrator.db
|
||||
# ORCH-042: live-tracker mode. edit (DEFAULT) -> the task card is edited in place
|
||||
# (editMessageText). bump -> on every update the old card is deleted and a fresh
|
||||
# one is sent silently to the BOTTOM of the chat (deleteMessage + sendMessage +
|
||||
# repoint). One card per task in both modes. Any value other than "bump" -> edit.
|
||||
ORCH_TRACKER_MODE=edit
|
||||
# ORCH-042/ORCH-067: live-tracker mode. bump (DEFAULT since ORCH-067) -> on every
|
||||
# update the old card is deleted and a fresh one is sent silently to the BOTTOM of
|
||||
# the chat (deleteMessage + sendMessage + repoint), so the current status is always
|
||||
# the last message in an active chat. edit -> the task card is edited in place
|
||||
# (editMessageText). One card per task in both modes. Any value other than "bump"
|
||||
# (incl. empty/garbage) -> edit.
|
||||
ORCH_TRACKER_MODE=bump
|
||||
# ORCH-067: best-effort live-overlay for the card status line. The offline core
|
||||
# (stage -> Plane status, In Review from the brd-clock) always works without network;
|
||||
# the overlay only fills in branches indistinguishable offline (Needs Input / Blocked /
|
||||
# Rejected / Cancelled / Deploying / Monitoring after Deploy) by reading the LIVE Plane
|
||||
# status with a short timeout + per-issue TTL cache. It NEVER blocks the pipeline and
|
||||
# NEVER raises.
|
||||
# LIVE_STATUS -> kill-switch (false -> offline core only).
|
||||
# LIVE_STATUS_TTL_S -> TTL (seconds) of the per-issue live-uuid cache (hot-path guard).
|
||||
# LIVE_STATUS_TIMEOUT_S -> timeout (seconds) of a single live-GET on the render path.
|
||||
ORCH_TRACKER_LIVE_STATUS=true
|
||||
ORCH_TRACKER_LIVE_STATUS_TTL_S=60
|
||||
ORCH_TRACKER_LIVE_STATUS_TIMEOUT_S=3
|
||||
# ORCH-043: merge-gate (auto-rebase onto current origin/main + re-test + merge-lock)
|
||||
# on the deploy-staging -> deploy edge. Deterministic sub-gate (no LLM) that catches
|
||||
# the branch up to the CURRENT origin/main, re-tests it, and serialises merges so two
|
||||
@@ -116,3 +130,86 @@ ORCH_RECONCILE_GRACE_DEFAULT_S=600
|
||||
ORCH_RECONCILE_GRACE_OVERRIDES_JSON=
|
||||
ORCH_RECONCILE_NOTIFY_UNBLOCK=true
|
||||
ORCH_RECONCILE_SKIP_BLOCKED_ENABLED=true
|
||||
|
||||
# ORCH-068: TTL (seconds) for the per-project Plane states cache (plane_sync
|
||||
# _STATES_CACHE). Historically the cache lived for the whole process lifetime,
|
||||
# so a status added to Plane after start was invisible until a restart
|
||||
# ("stale set -> no pipeline action"). With a TTL the entry self-heals by
|
||||
# re-fetching /states/ once it expires (reuses reload_project_states()).
|
||||
# >0 -> re-fetch after this many seconds (default 300 = 5 min);
|
||||
# 0 -> disable TTL -> strictly the previous lifetime cache (back-compat).
|
||||
ORCH_PLANE_STATES_TTL_S=300
|
||||
|
||||
# ORCH-065: job-reaper + proactive merge-lease reclaim. A background daemon thread
|
||||
# (src/job_reaper.py, started LAST in main.lifespan after requeue_running_jobs) reaps
|
||||
# zombie 'running' jobs whose monitor/process died before writing the terminal status
|
||||
# (one zombie at max_concurrency=1 blocks the whole shared queue) and periodically
|
||||
# reclaims dead/stale merge-leases. Liveness is three-tier: Tier-1 dead jobs.pid
|
||||
# (os.kill(pid,0)) after REAPER_DEAD_TICKS consecutive dead ticks (anti-false-positive
|
||||
# for a live agent); Tier-2 agent_runs.exit_code recorded but job still 'running'
|
||||
# (only after a REAPER_FINALIZE_GRACE_S finalization grace, so a live monitor still
|
||||
# doing git push / PR / Plane comments is never reaped); Tier-3 backstop after
|
||||
# REAPER_MAX_RUNNING_S. The terminal flip carries an atomic status='running' guard and
|
||||
# precedes any advance/enqueue (claim-before-act) so it never double-processes/-advances
|
||||
# a row racing a late monitor or requeue_running_jobs.
|
||||
# REAPER_ENABLED -> global kill-switch (false -> strictly prior behaviour).
|
||||
# REAPER_INTERVAL_S -> background scan period (seconds).
|
||||
# REAPER_DEAD_TICKS -> consecutive dead-pid ticks before reaping (Tier-1, >=2).
|
||||
# REAPER_MAX_RUNNING_S -> Tier-3 backstop ceiling; must exceed max agent_timeout+grace.
|
||||
# REAPER_FINALIZE_GRACE_S -> Tier-2 grace: how long agent_runs.exit_code must have been
|
||||
# recorded before a still-'running' job is reaped; MUST exceed
|
||||
# the max finalization window (git push + PR + Plane comments).
|
||||
# LEASE_RECLAIM_ENABLED -> kill-switch for the proactive stale/dead lease reclaim
|
||||
# (false -> only the legacy lazy TTL reclaim in acquire_merge_lease).
|
||||
# (reuse) ORCH_MERGE_LOCK_TIMEOUT_S -> lease TTL; ORCH_MERGE_GATE_REPOS -> reclaim scope.
|
||||
ORCH_REAPER_ENABLED=true
|
||||
ORCH_REAPER_INTERVAL_S=60
|
||||
ORCH_REAPER_DEAD_TICKS=2
|
||||
ORCH_REAPER_MAX_RUNNING_S=3600
|
||||
ORCH_REAPER_FINALIZE_GRACE_S=300
|
||||
ORCH_LEASE_RECLAIM_ENABLED=true
|
||||
|
||||
# ORCH-022: security-gate (secret-scanning + dependency audit) on the
|
||||
# deploy-staging -> deploy edge, run FIRST among the edge sub-gates. Deterministic
|
||||
# (no LLM): gitleaks (offline secret-scan, pinned Go binary in the image) + pip-audit
|
||||
# (OSV/PyPI CVE audit). Verdict in the versioned 17-security-report.md frontmatter;
|
||||
# FAIL -> rollback to development + developer-retry (cap 3). See ADR-001.
|
||||
# GATE_ENABLED -> global kill-switch; false -> pipeline 1:1 as before ORCH-022.
|
||||
# GATE_REPOS -> CSV of repos where the gate is REAL; empty -> only self-hosting.
|
||||
# DEP_BLOCK_SEVERITY -> CVE severity that BLOCKS (CRITICAL>HIGH>MEDIUM>LOW); below /
|
||||
# UNKNOWN -> warning only (anti-loop).
|
||||
# SCAN_TIMEOUT_S -> per external scanner call timeout.
|
||||
# DEP_AUDIT_FAIL_CLOSED -> strict mode: unreachable CVE feed -> FAIL instead of the
|
||||
# default fail-open + warning (anti-loop). Default false.
|
||||
# SECRETS_BLOCK -> a found secret blocks (always true by default; the offline
|
||||
# secrets guarantee is unconditional).
|
||||
ORCH_SECURITY_GATE_ENABLED=true
|
||||
ORCH_SECURITY_GATE_REPOS=
|
||||
ORCH_SECURITY_DEP_BLOCK_SEVERITY=HIGH
|
||||
ORCH_SECURITY_SCAN_TIMEOUT_S=300
|
||||
ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED=false
|
||||
ORCH_SECURITY_SECRETS_BLOCK=true
|
||||
|
||||
# ORCH-021: post-deploy production monitoring + degradation reaction. After the
|
||||
# terminal deploy->done transition for an applicable repo, a reserved-agent job
|
||||
# `post-deploy-monitor` (no LLM, modelled on deploy-finalizer) probes prod over a
|
||||
# window and reacts to a degradation the restart-time health-check missed (class
|
||||
# "green deploy, red prod", precedent ET-8). State is in sentinel files
|
||||
# (.post-deploy-state-<repo>/<wi>/), no DB migration.
|
||||
# MONITOR_ENABLED -> global kill-switch; false -> pipeline is 1:1 as before ORCH-021.
|
||||
# REPOS -> CSV of repos where monitoring is REAL; empty -> only self-hosting.
|
||||
# WINDOW_S -> observation window length (~15 min).
|
||||
# INTERVAL_S -> seconds between probe ticks.
|
||||
# FAIL_THRESHOLD -> N CONSECUTIVE health failures -> DEGRADED.
|
||||
# 5XX_THRESHOLD -> window 5xx ratio above this -> DEGRADED.
|
||||
# AUTO_ROLLBACK -> allow auto-rollback; acts ONLY for non-self repos. Self-hosting
|
||||
# is ALWAYS ALERT_ONLY (a tick NEVER restarts the prod container).
|
||||
# BASE_URL -> base URL of the observed prod instance.
|
||||
ORCH_POST_DEPLOY_MONITOR_ENABLED=true
|
||||
ORCH_POST_DEPLOY_REPOS=
|
||||
ORCH_POST_DEPLOY_WINDOW_S=900
|
||||
ORCH_POST_DEPLOY_INTERVAL_S=30
|
||||
ORCH_POST_DEPLOY_FAIL_THRESHOLD=3
|
||||
ORCH_POST_DEPLOY_5XX_THRESHOLD=0.5
|
||||
ORCH_POST_DEPLOY_AUTO_ROLLBACK=false
|
||||
ORCH_POST_DEPLOY_BASE_URL=http://localhost:8500
|
||||
|
||||
38
.gitleaks.toml
Normal file
38
.gitleaks.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
# gitleaks config — ORCH-022 security-gate (secret-scanning).
|
||||
#
|
||||
# Versioned in the repo root (07-infra I-4 / BR-13): rules + an allowlist of
|
||||
# known-safe matches are reviewed as code. The security-gate (src/security_gate.py)
|
||||
# passes this file via `--config` when present. gitleaks runs OFFLINE (local rules)
|
||||
# so the "a secret always blocks" guarantee (BR-2) never depends on the network.
|
||||
#
|
||||
# Strategy: extend the built-in ruleset (broad coverage, maintained upstream) and
|
||||
# only ADD a narrow allowlist for placeholders / fixtures that are intentionally
|
||||
# fake (e.g. .env.example dummy values, test fixtures). Keep the allowlist tight —
|
||||
# an over-broad allowlist silently re-opens the leak it was meant to bless.
|
||||
|
||||
title = "orchestrator gitleaks config"
|
||||
|
||||
[extend]
|
||||
# Start from gitleaks' maintained default ruleset.
|
||||
useDefault = true
|
||||
|
||||
[allowlist]
|
||||
description = "Known-safe, intentionally non-secret matches (placeholders + fixtures)."
|
||||
|
||||
# Files that legitimately contain placeholder/dummy secret-shaped values:
|
||||
# * .env.example — the committed canon of env vars with DUMMY values (CLAUDE.md §8;
|
||||
# real secrets live only in the host .env / .env.staging, never in git).
|
||||
# * tests/ — fixtures may embed fake tokens to exercise the scanner itself (TC-03).
|
||||
# * .gitleaks.toml — this file (avoid self-matching example patterns below).
|
||||
paths = [
|
||||
'''(^|/)\.env\.example$''',
|
||||
'''(^|/)tests/''',
|
||||
'''(^|/)\.gitleaks\.toml$''',
|
||||
]
|
||||
|
||||
# Generic placeholder tokens used in docs / examples that are NOT real secrets.
|
||||
regexes = [
|
||||
'''(?i)(your[-_]?(token|key|secret|password)[-_]?here)''',
|
||||
'''(?i)(changeme|dummy|example|placeholder|xxxxx+)''',
|
||||
'''(?i)<[a-z0-9_-]+>''',
|
||||
]
|
||||
@@ -91,6 +91,30 @@ The verdict contract is unchanged: `docs/work-items/<work_item_id>/14-deploy-log
|
||||
frontmatter field `deploy_status: SUCCESS|FAILED` (the gate `check_deploy_status` parses ONLY this).
|
||||
**What changed (ORCH-36): WHO and WHEN writes that verdict, for the self-hosting repo.**
|
||||
|
||||
### ⚠️ Idempotent merge guard — consult `pr_already_merged` BEFORE merging (ORCH-065)
|
||||
|
||||
The `deploy` stage can be **re-driven**: if a process/monitor thread died after the PR
|
||||
merged but before the job finalised, the job-reaper requeues it and this stage runs **again**
|
||||
(ADR-001 ORCH-065, Р-3). A blind second merge of an already-merged PR makes Gitea return a
|
||||
merge error → a false БАГ-8 rollback. To stay idempotent, **before you merge the feature
|
||||
branch PR into `main`, consult the deterministic guard** `merge_gate.pr_already_merged(repo, branch)`:
|
||||
|
||||
```bash
|
||||
# Already merged? exit 0 = yes (skip the merge), exit 1 = no (merge normally).
|
||||
python3 -c "import sys; from src.merge_gate import pr_already_merged; \
|
||||
sys.exit(0 if pr_already_merged('<repo>', '<branch>') else 1)" && MERGED=1 || MERGED=0
|
||||
```
|
||||
|
||||
- `MERGED=1` (PR already merged) → **do NOT merge again** (no second merge, no error).
|
||||
Treat the merge as already done and continue to write the deploy verdict
|
||||
(`deploy_status: SUCCESS` once the deploy itself is health-ok). This is the AC-11 no-op.
|
||||
- `MERGED=0` (not merged) → merge the PR normally, then proceed.
|
||||
|
||||
The guard is **never-raise** (any Gitea/parse error → `False` → "not known-merged", so a real
|
||||
merge is never silently skipped). This is the single consultation point ADR-001 Р-3 /
|
||||
README / CHANGELOG refer to: the **merge path (deployer/merge) consults the guard before a
|
||||
(repeat) merge**.
|
||||
|
||||
### Self-hosting repo (`orchestrator`) — you do NOT deploy yourself
|
||||
|
||||
For `orchestrator` the `deploy` stage is orchestrated by **deterministic code** in
|
||||
@@ -124,4 +148,7 @@ deploys go through `scripts/orchestrator-deploy-hook.sh` (parametrised; defaults
|
||||
|
||||
- Always write machine-readable YAML frontmatter — the quality gates parse ONLY the frontmatter fields, never the body prose.
|
||||
- Never push directly to `main`. Always use a PR or the artifact merge pattern.
|
||||
- **Idempotent merge (ORCH-065):** before any (re-)merge of a feature PR into `main`, consult
|
||||
`merge_gate.pr_already_merged(repo, branch)` (see the `deploy` stage section). Already merged
|
||||
→ no second merge, no error — the stage is a no-op on the merge and proceeds to its verdict.
|
||||
- Never modify `.env`, `.env.staging`, `docker-compose.yml`, or production infrastructure.
|
||||
|
||||
File diff suppressed because one or more lines are too long
27
CLAUDE.md
27
CLAUDE.md
@@ -38,16 +38,35 @@ created → analysis → architecture → development → review → testing →
|
||||
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3)
|
||||
```
|
||||
|
||||
## Статусная модель Plane (ORCH-066) — индикация ≠ управление
|
||||
Статусы Plane — это **слой B (индикация)**, отдельный от **слоя A (машина стадий)** `src/stages.py::STAGE_TRANSITIONS`. Plane показывает наблюдателю осмысленную картину (`Backlog → Todo → Analysis → Architecture → Development → Code-Review → Testing → Awaiting Deploy → Deploying → Monitoring after Deploy → Done` + человеческие гейты `In Review/Approved`, `Confirm Deploy`), но НИКОГДА не управляет конвейером. Маппинг и сеттеры — `src/plane_sync.py` (6 новых ключей: `to_analyse/analysis/code_review/awaiting_deploy/deploying/monitoring`), с project-relative alias-fallback: на частично сконфигурированном проекте новый ключ деградирует на базовый UUID ТОГО ЖЕ проекта (нулевая регрессия для enduro-trails). Детали — `docs/architecture/README.md`.
|
||||
|
||||
## Нотификации / Telegram live-tracker (ORCH-042/066/067)
|
||||
Каждая задача = **одна карточка** в Telegram (`src/notifications.py`). Поведение карточки:
|
||||
- **Дефолт `tracker_mode` — `bump`** (ORCH-067; `edit` доступен через `ORCH_TRACKER_MODE=edit`).
|
||||
`bump` на каждом обновлении удаляет старую карточку и шлёт свежую вниз чата (тихо), `edit`
|
||||
редактирует на месте. Инвариант «одна карточка на задачу» — в обоих режимах.
|
||||
- **Статус-строка карточки** (`📍 <status_label>`) показывает текущий Plane-статус по модели
|
||||
ORCH-066 (`plane_status_label`). Оффлайн-ядро (`stage → статус`, In Review из brd-clock)
|
||||
работает всегда без сети; best-effort live-overlay (kill-switch `tracker_live_status`,
|
||||
TTL-кэш, короткий таймаут) лишь дорисовывает ветки, неотличимые offline (Needs Input /
|
||||
Blocked / Rejected / Cancelled / Deploying / Monitoring) и **никогда не блокирует конвейер**.
|
||||
- **Кликабельный номер задачи** (`plane_issue_link`) — `ORCH-NNN` в карточке И во всех
|
||||
уведомлениях (`notify_*`, alert'ы стадий) рендерится как `<a href=…>` на issue в Plane;
|
||||
fail-safe → просто `html.escape(номер)`, если ссылку построить нельзя. Никогда не падает.
|
||||
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
|
||||
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
|
||||
|
||||
## Конвенции
|
||||
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
||||
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
|
||||
- ADR per work-item: `docs/work-items/<plane-id>/06-adr/ADR-NNN-slug.md`
|
||||
- Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md`
|
||||
- Work items: `docs/work-items/<plane-id>/`
|
||||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`), никогда проза
|
||||
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза
|
||||
|
||||
## Артефакты задачи (`docs/work-items/<plane-id>/`)
|
||||
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`.
|
||||
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`, `16-post-deploy-log.md` (post-deploy наблюдение, ORCH-021), `17-security-report.md` (security-гейт: `security_status:`/secrets/deps, ORCH-022).
|
||||
|
||||
## Правила для агентов
|
||||
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
|
||||
@@ -64,6 +83,10 @@ created → analysis → architecture → development → review → testing →
|
||||
- **НЕ перезапускать / не ронять прод-контейнер** `orchestrator` в рамках задачи — встанет конвейер всех проектов.
|
||||
- Любой деплой/рестарт self = групповой риск. Детали и топология — `docs/operations/INFRA.md`.
|
||||
- Стадия `deploy-staging` (порт 8501) — обязательная страховка перед прод-деплоем орка.
|
||||
- Прод-деплой орка запускается ТОЛЬКО переводом задачи на стадии `deploy` в выделенный
|
||||
Plane-статус **«Confirm Deploy»** (ORCH-059). Статус `Approved` — человеческий гейт
|
||||
конвейера и прод-деплой НЕ запускает (на `deploy` — no-op). Это разделяет «одобрить
|
||||
артефакт» и «выкатить в прод», чтобы привычный approve не ронял прод случайным кликом.
|
||||
|
||||
---
|
||||
*Паспорт проекта orchestrator. Поддерживается агентами при каждой доработке. Изолирован: описывает только этот проект (канон per-repo, см. ORCH-9).*
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -8,9 +8,28 @@ FROM python:3.12-slim
|
||||
ARG GIT_SHA=""
|
||||
LABEL org.opencontainers.image.revision=$GIT_SHA
|
||||
WORKDIR /app
|
||||
RUN apt-get update -qq && apt-get install -y -qq openssh-client git && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update -qq && apt-get install -y -qq openssh-client git curl ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
# git operations run as root over bind-mounted /repos (may be owned by host uid) -> trust it.
|
||||
RUN git config --system --add safe.directory '*'
|
||||
# ORCH-022: pinned gitleaks static Go binary for the offline secret-scan sub-gate
|
||||
# (07-infra I-1). Baked into the image (NOT a pip package): the gate runs INSIDE the
|
||||
# orchestrator container over a per-task worktree. Pinned release => deterministic
|
||||
# rules; gitleaks needs no network so the "a secret always blocks" guarantee (BR-2)
|
||||
# is independent of internet access. Multi-arch aware (amd64/arm64).
|
||||
ARG GITLEAKS_VERSION=8.18.4
|
||||
RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) gl_arch="x64" ;; \
|
||||
arm64) gl_arch="arm64" ;; \
|
||||
*) echo "unsupported arch: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL -o /tmp/gitleaks.tar.gz \
|
||||
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_${gl_arch}.tar.gz"; \
|
||||
tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks; \
|
||||
chmod +x /usr/local/bin/gitleaks; \
|
||||
rm -f /tmp/gitleaks.tar.gz; \
|
||||
gitleaks version
|
||||
# ORCH-58: compose runs the container as uid:gid 1000:1000 (ORCH-40), but the base
|
||||
# image has no passwd entry for uid 1000 -> ssh/whoami fail with
|
||||
# "No user exists for uid 1000" (rc=255), breaking the detached self-deploy ssh
|
||||
@@ -20,6 +39,13 @@ RUN groupadd -g 1000 app && useradd -u 1000 -g 1000 -m -d /home/slin -s /bin/bas
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY src/ ./src/
|
||||
COPY data/ ./data/
|
||||
# ORCH-021: do NOT `COPY data/ ./data/`. `data/` is gitignored (SQLite DB dir) and
|
||||
# is provided at runtime as a bind-mount volume (`./data:/app/data`, see
|
||||
# docker-compose.yml) which shadows anything baked into the image — so the COPY was
|
||||
# dead weight. Worse, the ORCH-058 staging rebuild (`check_staging_image_fresh`)
|
||||
# builds with the task *worktree* as the docker build context; a fresh worktree never
|
||||
# contains the untracked `data/`, so `COPY data/` failed `docker build` with exit 1
|
||||
# and bounced the task off `deploy-staging`. We just ensure the mountpoint exists.
|
||||
RUN mkdir -p /app/data
|
||||
ENV PYTHONPATH=/app
|
||||
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8500"]
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
|
||||
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance.
|
||||
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe.
|
||||
- **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`).
|
||||
- **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`.
|
||||
- **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 на сырой номер). Все алерты, упоминающие `work_item_id`, делают номер кликабельным. Контракт всего компонента — never raises; карточка всегда silent. Детали — [internals.md](internals.md) §7.
|
||||
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
|
||||
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane.
|
||||
- **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` неизменна (обратная совместимость).
|
||||
|
||||
## Конвейер и Quality Gates
|
||||
|
||||
@@ -35,7 +37,7 @@ created → analysis → architecture → development → review → testing →
|
||||
| deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) |
|
||||
| done | — | — | — |
|
||||
|
||||
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058).
|
||||
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058), check_security_gate (ORCH-022).
|
||||
|
||||
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`.
|
||||
|
||||
@@ -69,21 +71,25 @@ Self-hosting зацикливался на `deploy-staging`: `scripts/staging_ch
|
||||
а `deploy_status: SUCCESS` означает доказанный health-ok, не декларацию LLM. Три фазы
|
||||
(детерминированно, без LLM в критическом пути self-restart):
|
||||
- **Фаза A (вход в `deploy`)** — при `deploy_require_manual_approve=true` вместо запуска
|
||||
прод-deployer выставляется approval-pending статус Plane + запрос approve
|
||||
(Plane-коммент + Telegram). Перехват в `advance_stage` ПОСЛЕ `check_staging_status`
|
||||
и merge-gate.
|
||||
- **Фаза B (Plane → `Approved`)** — `advance_stage(deploy, finished_agent=None)`
|
||||
прод-deployer выставляется approval-pending статус Plane + запрос перевести задачу
|
||||
в статус **«Confirm Deploy»** (ORCH-059; Plane-коммент + Telegram). Перехват в
|
||||
`advance_stage` ПОСЛЕ `check_staging_status` и merge-gate.
|
||||
- **Фаза B (Plane → `Confirm Deploy`, ORCH-059)** —
|
||||
`advance_stage(deploy, finished_agent=None, confirm_deploy=True)`
|
||||
запускает **detached host-процесс** (ssh + setsid → хук с прод-параметрами +
|
||||
build-once retag `SOURCE_IMAGE`) и ставит детерминированный **finalizer-job**;
|
||||
маркер `initiated` — идемпотентность. Возврат БЕЗ advance (вердикта ещё нет).
|
||||
Обычный `Approved` на `deploy` (`confirm_deploy=False`) — детерминированный no-op
|
||||
(не деплоит и не откатывает).
|
||||
- **Фаза C (finalizer)** — новый контейнер после рестарта читает sentinel `result`
|
||||
(exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет `14-deploy-log.md`,
|
||||
вызывает `advance_stage(deploy, finished_agent="deployer")` → существующие контракты:
|
||||
`SUCCESS → done`, `FAILED → откат БАГ-8 на development`.
|
||||
|
||||
Approve = смена статуса Plane на `Approved` (status-only verdict model; комментарии
|
||||
не управляют конвейером). На старте — обязательный ручной approve (флаг `true`); полный
|
||||
авто — отдельная задача (ORCH-54). Условность как ORCH-35: реально для `orchestrator`,
|
||||
Триггер прод-деплоя = смена статуса Plane на `Confirm Deploy` (ORCH-059; status-only
|
||||
verdict model; комментарии не управляют конвейером). `Approved` остаётся исключительно
|
||||
человеческим гейтом конвейера и прод-деплой не запускает. На старте — обязательный
|
||||
ручной approve (флаг `true`); полный авто — отдельная задача (ORCH-54). Условность как ORCH-35: реально для `orchestrator`,
|
||||
прочие репо — прежний синхронный ssh-деплой агентом. Контракты не меняются:
|
||||
`STAGE_TRANSITIONS`, реестр QG, `check_deploy_status`/`_parse_deploy_status`, БАГ-8,
|
||||
terminal-sync, merge-gate, exit-code-контракт хука. Restart-safe состояние —
|
||||
@@ -91,6 +97,105 @@ sentinel-файлы (`<repos_dir>/.deploy-state-<repo>/<wi>/`), без мигр
|
||||
Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально —
|
||||
`docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`.
|
||||
|
||||
#### Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059 — реализовано)
|
||||
Перегрузка: один Plane-статус `Approved` служил И человеческим гейтом BRD на
|
||||
`analysis` (`check_analysis_approved`), И триггером Фазы B прод-деплоя на `deploy`
|
||||
— привычный жест approve молча запускал прод-рестарт (групповой self-hosting
|
||||
риск). ORCH-059 разделяет жесты: вводится отдельный логический статус
|
||||
`confirm_deploy` («Confirm Deploy»), который триггерит **ТОЛЬКО** Фазу B на
|
||||
`deploy`; `Approved` остаётся исключительно гейтом конвейера.
|
||||
- `_PLANE_NAME_TO_KEY` += `"Confirm Deploy" → "confirm_deploy"`; в
|
||||
`_DEFAULT_STATES` ключ НЕ добавляется (нет UUID для enduro/fallback) →
|
||||
**fail-closed**: нет статуса → нет деплоя, без `KeyError` (доступ через `.get`).
|
||||
- `handle_issue_updated` маршрутизирует `Confirm Deploy` → `handle_confirm_deploy`
|
||||
(гард `stage=="deploy"`) → `_try_advance_stage(..., confirm_deploy=True)`.
|
||||
- `advance_stage` получает kwarg `confirm_deploy: bool=False`; блок Фазы B
|
||||
(`deploy`+`finished_agent is None`+self-hosting) деплоит ТОЛЬКО при
|
||||
`confirm_deploy=True`, иначе (обычный `Approved`) — **no-op** (`check_deploy_status`
|
||||
не запускается → нет ложного отката БАГ-8).
|
||||
- CTA Фазы A (`_handle_self_deploy_phase_a`) просит «Confirm Deploy», не «Approved».
|
||||
- Условность как ORCH-35/36 (только `orchestrator`); Фазы A/C, `STAGE_TRANSITIONS`,
|
||||
`QG_CHECKS`, `check_deploy_status`, merge-gate, схема БД — без изменений.
|
||||
- Эксплуатация: в Plane-проекте ORCH создать статус «Confirm Deploy» + сброс кэша
|
||||
состояний (`docs/work-items/ORCH-059/07-infra-requirements.md`).
|
||||
|
||||
Детально — `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md`
|
||||
(уточняет/триггер Фазы B относительно adr-0007).
|
||||
|
||||
#### Merge-в-main + пост-деплой верификация как условие `done` (ORCH-071 — фикс фантомного merge)
|
||||
**Фантомный merge** (CRITICAL, постмортем `docs/history/LESSONS_2026-06-08_phantom-merge.md`):
|
||||
на self-hosting пути `deploy` агент `deployer` НЕ запускается, а фактический merge PR в `main`
|
||||
исторически делал ТОЛЬКО он → детерминированный путь
|
||||
(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`) **не содержал шага
|
||||
merge-в-main вообще**. Detached host-деплой лишь retag'ал образ + рестартил 8500; `done`
|
||||
достигался по `deploy_status: SUCCESS` без верификации `main`. Зелёный деплой (образ из рабочей
|
||||
ветки) маскировал отсутствие merge → следующая задача срезала ветку от устаревшего `main` и
|
||||
теряла код предшественника (накопительно потеряны ORCH-022/059/066/068). ORCH-071 вводит
|
||||
**детерминированный merge-актор + пост-merge верификацию** как **под-гейт ребра `deploy → done`**
|
||||
(симметрично edge-под-гейтам `deploy-staging → deploy`), только для self-hosting:
|
||||
- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и
|
||||
`next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`). Гейтит
|
||||
**ВСЕ** пути к `done` единообразно (`run_deploy_finalizer` Phase C, reconciler F-1, job-reaper —
|
||||
все идут через `advance_stage`), закрывая дыру обхода merge.
|
||||
- **Merge в Phase C (после рестарта), НЕ в Phase B** — finalizer restart-surviving (claim воркером
|
||||
нового контейнера, re-drive reaper'ом), merge физически строго ПОСЛЕ рестарта прода → рестарт его
|
||||
не убивает (G3 «шаг, переживающий рестарт»; постмортем-урок №3).
|
||||
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (no-op повтор, ORCH-065) → иначе
|
||||
Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Никогда push/force-push в `main`.
|
||||
- **Верификатор `merge_gate.verify_merged_to_main`** — `PR.merged==true` ИЛИ
|
||||
`git merge-base --is-ancestor <validated_sha> origin/main` (`validated_revision` — тот же якорь,
|
||||
что у ORCH-058). never-raise → `False`.
|
||||
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
|
||||
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged есть
|
||||
инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → штатный `deploy →
|
||||
done` + `merged_to_main: true` во frontmatter `14-deploy-log.md` (`deploy_status:` нетронут).
|
||||
- **Условность как ORCH-35/43/58:** `merge_verify_enabled` (kill-switch, дефолт `true`) +
|
||||
`merge_verify_repos` (пусто → только self-hosting); non-self — no-op, merge остаётся за `deployer`.
|
||||
never-raise; идемпотентность (`pr_already_merged`, INV-5); ручной approve сохранён (`Confirm Deploy`).
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, реестр
|
||||
`QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG), схема БД,
|
||||
БАГ-8, terminal-sync, merge-gate, image-freshness, exit-коды хука — **без изменений**.
|
||||
Диагностика фантома — runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки постмортема).
|
||||
|
||||
Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md), детально —
|
||||
`docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`.
|
||||
|
||||
### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано)
|
||||
Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check
|
||||
в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 —
|
||||
деградация через минуты под трафиком, health `200 ok`, фича сломана). ORCH-021 продлевает
|
||||
ответственность **ЗА** `done`: для применимого репо после терминального перехода армится
|
||||
наблюдение окна `post_deploy_window_s` (~15 мин) с интервалом `post_deploy_interval_s`;
|
||||
деградация фиксируется по детерминированным порогам, при подтверждении — реакция.
|
||||
|
||||
Механизм — **reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`, НЕ
|
||||
стадия и НЕ daemon): арм в `advance_stage` в блоке `next_stage == "done"`
|
||||
(`post_deploy.arm_monitor`, sentinel `armed` = идемпотентность); тик перехватывается в
|
||||
`launcher.launch_job` ДО `_spawn` → `stage_engine.run_post_deploy_monitor` (один опрос →
|
||||
append в `series` → классификация → перепостановка с задержкой ИЛИ реакция+артефакт+`done`).
|
||||
Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise): `post_deploy_applies`,
|
||||
`probe_signals` (`/health` 200+`{"status":"ok"}` + доля 5xx на `/status`,`/queue`),
|
||||
`classify` (HEALTHY|DEGRADED — главный предмет юнит-тестов), `decide_action`,
|
||||
sentinel-state, `write_post_deploy_log`.
|
||||
- **Пороги (BR-3):** `DEGRADED` ⇔ `≥ post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов
|
||||
health ИЛИ доля 5xx `> post_deploy_5xx_threshold`; одиночный глюк → HEALTHY (нет ложных
|
||||
откатов).
|
||||
- **Реакция:** self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY` (Telegram+Plane, ручной
|
||||
approve; тик НИКОГДА не откатывает/рестартит прод-контейнер); не-self +
|
||||
`post_deploy_auto_rollback=true` → хук `--rollback` (`0→ROLLBACK_OK`,
|
||||
`1/2→ROLLBACK_FAILED`+алерт); дефолт → `ALERT_ONLY`.
|
||||
- **Артефакт** `16-post-deploy-log.md` (YAML-frontmatter `post_deploy_status`/
|
||||
`action_taken`/…) — машиночитаемо для петли уроков ORCH-8; best-effort.
|
||||
- **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец `reconcile`).
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, terminal-sync,
|
||||
merge-gate, exit-коды хука (0/1/2), схема БД — НЕ меняются. Restart-safe (sentinel
|
||||
`.post-deploy-state-<repo>/<wi>/` + jobs-очередь). Kill-switch
|
||||
`post_deploy_monitor_enabled`, область `post_deploy_repos` (пусто → self-hosting).
|
||||
Условность как ORCH-35/36/43/58.
|
||||
|
||||
Подробнее: [adr-0010](adr/adr-0010-post-deploy-monitor.md), детально —
|
||||
`docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`.
|
||||
|
||||
### Свежесть артефакта BUILD-ONCE: провенанс staging-образа (ORCH-058 — реализовано)
|
||||
BUILD-ONCE retag (ORCH-36) промоутит `SOURCE_IMAGE=orchestrator-orchestrator-staging` в прод
|
||||
**без rebuild**, полагаясь на «staging-образ свеж и провалидирован». Этой гарантии нет:
|
||||
@@ -118,6 +223,38 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
|
||||
образа, без миграций). Подробнее: [adr-0008](adr/adr-0008-staging-image-provenance.md),
|
||||
детально — `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.
|
||||
|
||||
### Security-гейт: secret-scanning + dependency audit перед мержем (ORCH-022 — реализовано)
|
||||
Автономный конвейер вливал ветку в `main` без проверки на утёкший секрет (ключ/токен/пароль/
|
||||
приватный ключ) и уязвимую зависимость (CVE); для self-hosting один секрет/CVE через одну
|
||||
задачу уезжал в общий прод всех проектов (CLAUDE.md §8). ORCH-022 вводит детерминированный
|
||||
(без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, рядом с merge-gate
|
||||
(ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди edge-под-гейтов
|
||||
(ДО merge-gate). Паттерн соседей: leaf `src/security_gate.py` (never-raise) + тонкая обёртка
|
||||
`check_security_gate` в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
|
||||
`STAGE_TRANSITIONS` и схема БД — **без изменений**.
|
||||
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
|
||||
аллоулиста `.gitleaks.toml` → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
|
||||
не зависит от сети (безусловна).
|
||||
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
|
||||
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
|
||||
громкий warning** (анти-петля ORCH-061; флаг `security_dep_audit_fail_closed` для строгого
|
||||
режима). best-effort при доступности фида.
|
||||
- **ПЕРВЫМ, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки ДО rebase
|
||||
не «обвиняет» задачу в CVE из обновившегося `main`; до захвата merge-lease → при FAIL lease
|
||||
освобождать не нужно.
|
||||
- **Артефакт `17-security-report.md`** (YAML-frontmatter `security_status`/`secrets_found`/
|
||||
`deps_blocking`/`deps_warning`/`deps_audit_degraded`); вердикт читается ТОЛЬКО из
|
||||
frontmatter (гейт пишет → читает обратно через `parse_security_status` → возвращает: единый
|
||||
источник истины), negative-токен авторитетен, битый/нет → fail-closed.
|
||||
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
|
||||
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
|
||||
- **Условность как ORCH-35/43/58:** `security_gate_enabled` + `security_gate_repos` (пусто →
|
||||
только self-hosting); never-raise; таймаут `security_scan_timeout_s`; гейт не деплоит/не
|
||||
рестартит прод. v1 — Python-only; SAST/мульти-стек — follow-up (BR-14).
|
||||
|
||||
Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально —
|
||||
`docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`.
|
||||
|
||||
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
|
||||
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
|
||||
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
|
||||
@@ -137,11 +274,21 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
|
||||
retry-count проверяется первым (дёшево, локальный SQL).
|
||||
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
|
||||
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
|
||||
**ORCH-068 (livelock-fix):** (1) задачи в **терминальной группе** Plane
|
||||
(`state.group ∈ {completed, cancelled}`, fallback — логические ключи
|
||||
`done`/`cancelled`) исключаются из actionable-выборки per-issue — проектно-независимо,
|
||||
устойчиво к UUID-алиасингу после переименований статусов (ORCH-066); (2) `_note_unblock`
|
||||
(лог + Telegram + `unblocked_total`) вызывается ТОЛЬКО при **подтверждённом state change**
|
||||
(сравнение стадии задачи до/после `_dispatch`; no-op dispatch → тишина), плюс in-memory
|
||||
дедуп по `issue_id→state`. Восстанавливает инвариант silence-when-in-sync (AC-9/AC-10).
|
||||
Детали — `docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`.
|
||||
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной
|
||||
development-задаче repo; неоднозначность → не резолвим).
|
||||
- **F-4 observability:** при разблокировке — лог-строка `reconciler: <wi> <stage>
|
||||
разблокирована (потерян webhook)` + Telegram (`reconcile_notify_unblock`); снимок
|
||||
состояния в `GET /queue` (блок `reconcile`).
|
||||
состояния в `GET /queue` (блок `reconcile`). **ORCH-068** добавляет в снимок
|
||||
счётчики `skipped_terminal_total` (исключённые терминалы) и `deduped_total`
|
||||
(подавленные повторные нотификации).
|
||||
|
||||
Реализация: `src/reconciler.py` (daemon-поток по образцу `queue_worker`), стартует в
|
||||
`main.lifespan` **после** `worker.start()`, останавливается в `finally` **перед**
|
||||
@@ -154,6 +301,104 @@ never-raise на единицу работы; тишина при синхрон
|
||||
и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются. Подробнее:
|
||||
[adr-0007](adr/adr-0007-reconciler.md), детально — `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`.
|
||||
|
||||
### Job-reaper + проактивный реклейм merge-lease (ORCH-065 — design)
|
||||
Финализация статуса job (`done`/`queued`/`failed`) выполняется ТОЛЬКО в
|
||||
`launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляла строку `jobs` навсегда `running`; при
|
||||
`max_concurrency=1` одна зомби-строка блокирует claim всех job → встаёт конвейер
|
||||
ВСЕХ проектов (инциденты 07.06: jobs 236/239/242/254). `requeue_running_jobs()`
|
||||
спасал ТОЛЬКО на старте процесса. Симметрично залипал merge-lease (ORCH-043):
|
||||
реклейм был лениво-по-TTL и только при чужом `acquire`, liveness держателя по pid
|
||||
не проверялся. Это последняя ручная точка автономного self-deploy (блокер ORCH-54).
|
||||
ORCH-065 вводит фоновый watchdog, чтобы смерть процесса/потока на любой стадии НЕ
|
||||
оставляла навсегда захваченных ресурсов:
|
||||
- **Job-reaper** (`src/job_reaper.py`) — daemon-поток по образцу `reconciler`,
|
||||
работает **без рестарта**. Трёхуровневая liveness: Tier-1 мёртвый `jobs.pid`
|
||||
(новая колонка) после `reaper_dead_ticks` подряд тиков (анти-ложноположительность
|
||||
— живой долгий агент не реапится); Tier-2 `agent_runs.exit_code` записан, а job
|
||||
ещё `running` — но это окно неоднозначно (живой monitor пишет exit_code ПЕРВЫМ,
|
||||
затем git push/PR/Plane-комментарии), поэтому Tier-2 реапит только после
|
||||
finalization-grace `reaper_finalize_grace_s` (живой финализирующий monitor НЕ
|
||||
реапится); Tier-3 backstop по потолку `reaper_max_running_s` (> max
|
||||
agent_timeout+grace). Действие переиспользует контракты по принципу
|
||||
**claim-before-act**: для exit0 канонический QG оценивается read-only ПЕРЕД
|
||||
атомарным claim, затем claim `done` ПЕРВЫМ и только победитель claim делает
|
||||
`_try_advance_stage` (advance+enqueue) — проигравший claim (поздний monitor /
|
||||
стартовый requeue) не выполняет побочных эффектов (нет дубль-advance/-enqueue);
|
||||
источник истины — канонический QG, не факт «exit0»; гейт красный или exit≠0/
|
||||
неизвестно → `attempts<max`→`queued`, иначе `failed`+Telegram. Атомарный
|
||||
reap-claim (`UPDATE ... WHERE id=? AND status='running'`) совместим со стартовым
|
||||
`requeue_running_jobs` (restart-safe, без двойной обработки).
|
||||
- **Проактивный реклейм stale/dead lease** (функции в `merge_gate.py`:
|
||||
`pid_alive`, `reclaim_stale_lease`) — на старте (рядом с `requeue_running_jobs`)
|
||||
и периодически из тика reaper: освобождает lease, чей держатель **мёртв** (pid
|
||||
не жив) ИЛИ **просрочен** (TTL `merge_lock_timeout_s`); живой держатель в
|
||||
пределах TTL — НЕ трогать (защита легитимного merge). holder-aware, never-raise,
|
||||
условность как ORCH-43 (`merge_gate_repos`/self-hosting).
|
||||
- **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
|
||||
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не повторяются
|
||||
(`branch_is_behind_main==False`); добавлен never-raise guard `pr_already_merged`
|
||||
(читает состояние PR) — уже слит = no-op. **Консультируется самим merge-актором:**
|
||||
фактический merge PR в `main` делает агент `deployer` (в начале стадии `deploy`),
|
||||
поэтому wiring — в его промпте `.openclaw/agents/deployer.md`, который вызывает
|
||||
`pr_already_merged` ПЕРЕД любым (повторным) merge (AC-11). Чек `check_branch_mergeable`
|
||||
НЕ меняется (AC-13): он на ПЕРВОМ ребре `deploy-staging → deploy`, а риск второго
|
||||
merge — на re-drive самой стадии `deploy`.
|
||||
- **Схема БД:** единственное изменение — `jobs.pid INTEGER` через идемпотентный
|
||||
`_ensure_column` (live-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука, файл-схема lease — без изменений.
|
||||
- **Наблюдаемость:** блок `reaper` в `GET /queue` (enabled, interval, last_run_ts,
|
||||
reaped_total, last_reaped, lease_reclaimed_total); каждый reap/lease-reclaim →
|
||||
`logger.warning`; reap→`failed` и lease-reclaim → Telegram.
|
||||
- **Kill-switch'и:** `ORCH_REAPER_ENABLED`, `ORCH_REAPER_INTERVAL_S`,
|
||||
`ORCH_REAPER_DEAD_TICKS`, `ORCH_REAPER_MAX_RUNNING_S`,
|
||||
`ORCH_REAPER_FINALIZE_GRACE_S`, `ORCH_LEASE_RECLAIM_ENABLED`; `false` → строго
|
||||
прежнее поведение.
|
||||
|
||||
Подробнее: [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md), детально —
|
||||
`docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md`.
|
||||
|
||||
### Осмысленная статусная модель Plane (ORCH-066 — реализовано)
|
||||
Plane-доска была семантически перегружена: `In Progress` означал «человек запускает
|
||||
конвейер», «идёт анализ», «идёт прод-деплой» и «возврат из Needs Input» одновременно.
|
||||
ORCH-066 наводит порядок по утверждённой Owner модели, меняя **только слой B**
|
||||
(Plane-индикация: `src/plane_sync.py` + точки простановки в `src/stage_engine.py`/
|
||||
`src/webhooks/plane.py`/`src/reconciler.py`) и **не трогая слой A** (`STAGE_TRANSITIONS`,
|
||||
инвариант). Статус — индикация, не управление (вердикты по-прежнему из YAML-frontmatter):
|
||||
```
|
||||
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
|
||||
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
|
||||
Monitoring after Deploy → Done
|
||||
```
|
||||
`[...]` = человеческий вход-триггер; остальное ставит орк.
|
||||
- **6 новых логических ключей** (`to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/
|
||||
`deploying`/`monitoring`) в `_PLANE_NAME_TO_KEY` (резолв по имени) + `_DEFAULT_STATES`.
|
||||
`To Analyse` заменяет `In Progress` как вход-триггер (старт + resume аналитика из Needs
|
||||
Input; fork «старт vs resume» по `get_task_by_plane_id`+`has_active_job_for_task` —
|
||||
сохранён). Стадии: analysis→`Analysis`, review→`Code-Review` (`_STAGE_TO_STATE_KEY`).
|
||||
- **Self-deploy фазы:** Phase A → `Awaiting Deploy` (разгружает `In Review`), Phase B →
|
||||
`Deploying`, Phase C/terminal-sync (self) → `Monitoring after Deploy` (НЕ `Done` сразу);
|
||||
post-deploy monitor (ORCH-021): HEALTHY-окно → `Done`, DEGRADED → `Blocked` (тик
|
||||
по-прежнему НИКОГДА не рестартит прод — ALERT_ONLY). Не-self репо: `deploy → Done` как
|
||||
сейчас (terminal-sync разводится по `post_deploy.post_deploy_applies`).
|
||||
- **Fail-closed (project-relative alias-fallback):** отсутствующий новый статус в проекте
|
||||
деградирует на **собственный базовый UUID того же проекта** (`to_analyse/analysis→in_progress`,
|
||||
`code_review→review`, `awaiting_deploy→in_review`, `deploying→in_progress`,
|
||||
`monitoring→done`) — индикация откатывается к текущей, конвейер не ломается, PATCH валиден
|
||||
даже при частичной конфигурации. Enduro (статусы не создаются) → строго прежнее поведение.
|
||||
Усиленный паттерн ORCH-059 AC-7.
|
||||
- **Reconciler:** F-2 триггер `in_progress`→`to_analyse`; Guard 2 skip-set расширен
|
||||
активными ожиданиями (`awaiting_deploy`/`deploying`/`monitoring`) с **вычитанием базовых
|
||||
рабочих статусов** — на enduro (алиасы схлопнуты) нулевой регресс, на orchestrator skip
|
||||
реальных ожиданий (BR-13).
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, exit-коды хука,
|
||||
merge-gate, `Confirm Deploy`, механизм `Needs Input` (analyst-only), схема БД — без
|
||||
изменений. Без нового kill-switch (раскат гейтится созданием Plane-статусов оператором).
|
||||
Инфра-предусловие — `docs/work-items/ORCH-066/07-infra-requirements.md`.
|
||||
|
||||
Подробнее: `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`.
|
||||
|
||||
## Откаты
|
||||
- Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`).
|
||||
- Tester `check_tests_passed` FAIL → откат на `development` + retry.
|
||||
@@ -187,7 +432,7 @@ never-raise на единицу работы; тишина при синхрон
|
||||
- `events` — входящие вебхуки (дедуп)
|
||||
- `tasks` — задачи и их стадии
|
||||
- `agent_runs` — запуски агентов (run_id, usage, cost)
|
||||
- `jobs` — очередь задач (ORCH-1)
|
||||
- `jobs` — очередь задач (ORCH-1); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
|
||||
|
||||
## Изоляция (git worktree, ORCH-2)
|
||||
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
|
||||
@@ -197,7 +442,7 @@ never-raise на единицу работы; тишина при синхрон
|
||||
|--------|------|----------|
|
||||
| GET | `/health` | health check |
|
||||
| GET | `/status` | активные задачи (stage != done) |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + последние jobs |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + последние jobs |
|
||||
| POST | `/webhook/plane` | Plane webhook |
|
||||
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |
|
||||
|
||||
@@ -211,4 +456,7 @@ never-raise на единицу работы; тишина при синхрон
|
||||
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
|
||||
|
||||
---
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled).*
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-022 (security-гейт: secret-scanning gitleaks + dependency audit pip-audit как под-гейт ребра `deploy-staging → deploy` ПЕРВЫМ, adr-0012, `docs/work-items/ORCH-022/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-022-security-secret-scanning (leaf src/security_gate.py never-raise + check_security_gate в src/qg/checks.py `QG_CHECKS` + врезка _handle_security_gate в src/stage_engine.py блок `current_stage == "deploy-staging"` ПЕРВОЙ; флаги `security_*` в src/config.py; gitleaks (pinned) в Dockerfile, pip-audit в requirements.txt, `.gitleaks.toml` в корне; артефакт 17-security-report.md; обновлять также при изменении этих мест).*
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-059 (выделенный статус-триггер прод-деплоя «Confirm Deploy», ADR `docs/work-items/ORCH-059/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-059 (маппинг `"Confirm Deploy"→"confirm_deploy"` в src/plane_sync.py `_PLANE_NAME_TO_KEY`, НЕ в `_DEFAULT_STATES` = fail-closed; ветка `handle_confirm_deploy` + fail-closed `.get("confirm_deploy")` в src/webhooks/plane.py `handle_issue_updated`; keyword-only `confirm_deploy` в src/stage_engine.py `advance_stage` — Фаза B деплоит ТОЛЬКО при `confirm_deploy=True`, иначе `Approved`-на-`deploy` = no-op; CTA Фазы A просит «Confirm Deploy»; эксплуатация — статус доски «Confirm Deploy» в Plane-проекте ORCH, `docs/work-items/ORCH-059/07-infra-requirements.md`).*
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-066 (осмысленная статусная модель Plane — слой B, `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`) — реализовано в ветке feature/ORCH-066-plane (только Plane-индикация: новые ключи `to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/`deploying`/`monitoring` в `_PLANE_NAME_TO_KEY`/`_DEFAULT_STATES` + project-relative `_STATE_ALIAS_FALLBACK` в get_project_states + `_STAGE_TO_STATE_KEY` analysis/review + 5 новых `set_issue_*` в src/plane_sync.py; триггер `in_progress`→`to_analyse` и `set_issue_analysis` в src/webhooks/plane.py; Phase A→Awaiting Deploy / Phase B→Deploying / terminal-sync split monitoring↔done / post-deploy monitor HEALTHY→Done DEGRADED→Blocked в src/stage_engine.py; F-2 триггер `to_analyse` + Guard 2 skip-set с вычитанием base_working в src/reconciler.py; `STAGE_TRANSITIONS`/QG/схема БД НЕ трогаются; без kill-switch — раскат гейтится созданием 6 Plane-статусов оператором, `docs/work-items/ORCH-066/07-infra-requirements.md`; обновлять при изменении этих мест).*
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-068 (livelock-fix reconciler F-2: терминал-исключение по группе состояния + `_note_unblock` только при подтверждённом state change + дедуп; TTL `_STATES_CACHE`, `docs/work-items/ORCH-068/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-068 (D1 терминал-гард по группе `_is_terminal_state` + `get_project_state_groups` в src/plane_sync.py; D2 сравнение стадии до/после `_dispatch` + дедуп-словарь в src/reconciler.py; TTL-запись `_STATES_CACHE` + флаг `plane_states_ttl_s` в src/config.py; счётчики `skipped_terminal_total`/`deduped_total` в `/queue`; обновлять также при изменении src/reconciler.py F-2, src/plane_sync.py `get_project_states`/`get_project_state_groups`/`_STATES_CACHE`).*
|
||||
|
||||
@@ -15,11 +15,13 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
|
||||
| adr-0007 | Исполняемый самодеплой стадии `deploy` (файл adr-0007-executable-self-deploy) | accepted | 2026-06-06 | ORCH-036 |
|
||||
| adr-0008 | Провенанс staging-образа перед BUILD-ONCE retag | accepted | 2026-06-06 | ORCH-058 |
|
||||
| adr-0009 | Толерантность staging-вердикта к инфраструктурным FAIL | accepted | 2026-06-07 | ORCH-061 |
|
||||
| adr-0010 | Post-deploy мониторинг прода + реакция на деградацию | proposed | 2026-06-07 | ORCH-021 |
|
||||
| adr-0011 | Job-reaper + проактивный реклейм merge-lease | accepted | 2026-06-07 | ORCH-065 |
|
||||
|
||||
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
|
||||
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
|
||||
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
|
||||
> свободный номер (текущий максимум — `0009`).
|
||||
> свободный номер (текущий максимум — `0011`).
|
||||
|
||||
## Формат
|
||||
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.
|
||||
|
||||
@@ -69,6 +69,15 @@ grace + `max_concurrency=1`); never-raise на единицу работы; ти
|
||||
задачи. Инварианты adr-0007 сохранены (схема/реестры не меняются, never-raise,
|
||||
тишина при пропуске).
|
||||
|
||||
- **ORCH-068** (`docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`):
|
||||
фикс livelock F-2 (спам `_note_unblock` по синхронизированной done-задаче после
|
||||
ORCH-066). F-2 исключает терминалы по **группе состояния** (`completed`/`cancelled`,
|
||||
fallback — ключи `done`/`cancelled`) проектно-независимо; `_note_unblock` — только при
|
||||
подтверждённом state change (сравнение стадии до/после `_dispatch`) + in-memory дедуп;
|
||||
`_STATES_CACHE` получает TTL (`ORCH_PLANE_STATES_TTL_S`, дефолт 300с, `0`=lifetime).
|
||||
Инварианты adr-0007 сохранены (источник истины — Plane; реестры/схема/`handle_*`/F-1/F-3
|
||||
не меняются; never-raise; kill-switch'и).
|
||||
|
||||
## Связи
|
||||
adr-0002 (очередь / `available_at`, single-process-singleton), adr-0003 (условный
|
||||
гейт — образец условности/флагов раската), adr-0006 (merge-gate как под-гейт ребра
|
||||
|
||||
85
docs/architecture/adr/adr-0010-post-deploy-monitor.md
Normal file
85
docs/architecture/adr/adr-0010-post-deploy-monitor.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# adr-0010: Post-deploy мониторинг прода + реакция на деградацию
|
||||
|
||||
- **Статус:** proposed (design) — реализация в ветке `feature/ORCH-021-post-deploy-rollback`
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-021
|
||||
- **Метка:** `arch:major-change` (новая под-компонента + новый reserved-agent job-kind)
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`
|
||||
|
||||
## Контекст
|
||||
Конвейер заканчивается на `deploy → done`: `check_deploy_status` видит
|
||||
`deploy_status: SUCCESS` → terminal-sync (Plane → Done, release merge-lease), и
|
||||
оркестратор **забывает про прод**. «Успех» сегодня = health-check в момент рестарта
|
||||
(~60с окно в `orchestrator-deploy-hook.sh`). Класс инцидентов «зелёный деплой, красный
|
||||
прод» (прецедент **ET-8**): деградация проявляется через минуты под боевым трафиком,
|
||||
health отвечает `200 ok`, фича сломана. Для self-hosting опасно вдвойне — сломанный
|
||||
прод-орк (8500) обслуживает ВСЕ проекты из общего инстанса.
|
||||
|
||||
## Решение
|
||||
Продлить ответственность конвейера **ЗА** `done`: после терминального перехода для
|
||||
применимого репо армится пост-деплой наблюдение окна `post_deploy_window_s` (дефолт
|
||||
~15 мин) с интервалом `post_deploy_interval_s`; деградация фиксируется по
|
||||
**детерминированным порогам**, при подтверждении выполняется реакция.
|
||||
|
||||
**Механизм — reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`,
|
||||
ORCH-36), НЕ отдельная стадия и НЕ daemon-поток:
|
||||
- **Арм:** в `stage_engine.advance_stage`, в блоке `next_stage == "done"`, при
|
||||
`post_deploy.post_deploy_applies(repo)` → `post_deploy.arm_monitor(...)` (sentinel
|
||||
`armed` = идемпотентность, первый job через `enqueue_job(available_at_delay_s=...)`).
|
||||
- **Тик:** `launcher.launch_job` перехватывает `agent == "post-deploy-monitor"` ДО
|
||||
`_spawn` → `stage_engine.run_post_deploy_monitor(job)`: один опрос сигналов, append в
|
||||
персистентный `series`, классификация; HEALTHY и окно не истекло → перепостановка с
|
||||
задержкой; иначе → реакция + артефакт + `mark_done`.
|
||||
- **Чистая логика — новый leaf-модуль `src/post_deploy.py`** (never-raise, по образцу
|
||||
`self_deploy.py`/`staging_verdict.py`): `post_deploy_applies`, `probe_signals`
|
||||
(опрос `/health` + доля 5xx на `/status`,`/queue`), `classify` (HEALTHY|DEGRADED —
|
||||
главный предмет юнит-тестов), `decide_action` (NONE|ROLLBACK|ALERT_ONLY с учётом
|
||||
self-hosting), sentinel-state хелперы, `write_post_deploy_log`.
|
||||
|
||||
**Сигналы и пороги (детерминированно, AC-3…AC-6):** `DEGRADED` ⇔ `≥
|
||||
post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов health ИЛИ доля 5xx на окне `>
|
||||
post_deploy_5xx_threshold`. Одиночный глюк < порога → HEALTHY (нет ложных откатов).
|
||||
|
||||
**Реакция (BR-4/BR-5):**
|
||||
- **Self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY`:** громкий Telegram + Plane,
|
||||
запрос ручного approve отката. Тик НИКОГДА не откатывает/рестартит прод-контейнер
|
||||
(структурный инвариант). Откат прод-орка, если оператор решит, — только detached
|
||||
host-процесс (`self_deploy.initiate_deploy`), вне тика (MVP).
|
||||
- **Не-self + `post_deploy_auto_rollback=True`:** хук `--rollback` с прод-env; exit
|
||||
`0 → ROLLBACK_OK`, `1/2 → ROLLBACK_FAILED` + громкий алерт.
|
||||
- Дефолт (`auto_rollback=False`) → `ALERT_ONLY`.
|
||||
|
||||
**Артефакт `16-post-deploy-log.md`** (новый) с YAML-frontmatter (`post_deploy_status`,
|
||||
`action_taken`, `window_s`, `checks_total/failed`) — машиночитаемо для петли уроков
|
||||
ORCH-8; best-effort. **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец
|
||||
`reconcile.status()`).
|
||||
|
||||
## Альтернативы
|
||||
- **Daemon-watchdog (как reconciler)** — отклонён: per-task серия опросов в памяти не
|
||||
restart-safe (а деплой орка = рестарт); restart-safe-вариант требует тех же sentinel,
|
||||
reserved-agent проще и уже имеет проверенную jobs+sentinel машинерию.
|
||||
- **Отдельная пост-deploy стадия + QG** — отклонён: меняет `STAGE_TRANSITIONS`/
|
||||
`QG_CHECKS`, ломает семантику терминального `done`; наблюдение принципиально ПОСЛЕ
|
||||
`done`.
|
||||
- **Авто-rollback прод-орка из тика** — отклонён (self-hosting safety): групповой риск;
|
||||
контейнер не откатит себя надёжно. Self → alert + ручной approve (как ORCH-54).
|
||||
- **Колонка в `tasks`** — отклонён: миграция на проде; sentinel-файлы restart-safe
|
||||
(как ORCH-36/53/58).
|
||||
|
||||
## Последствия
|
||||
- Класс «зелёный деплой, красный прод» закрыт измеримыми порогами; деградация =
|
||||
сигнал для ORCH-8.
|
||||
- Реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`), контракт `check_deploy_status`,
|
||||
terminal-sync, merge-gate, exit-code-контракт хука, схема БД — **не меняются**.
|
||||
- Дефолты безопасны: kill-switch on, auto-rollback off, self только alert.
|
||||
- Ограничение: монитор self бежит внутри наблюдаемого прода — полностью wedged
|
||||
контейнер = пропущенный тик/алерт (known MVP gap; внешний watchdog — follow-up).
|
||||
- Self-hosting: тик не рестартит/не роняет прод-контейнер; kill-switch
|
||||
`post_deploy_monitor_enabled` обязателен; поэтапный раскат через `post_deploy_repos`.
|
||||
|
||||
## Связи
|
||||
adr-0007-executable-self-deploy (ORCH-36 — sentinel/detached-host/finalizer образец,
|
||||
`map_exit_code_to_status`), adr-0007-reconciler (ORCH-53 — daemon/`status()` образец,
|
||||
отклонён как основной механизм), adr-0006 (merge-gate — условность/флаги раската),
|
||||
adr-0003 (staging-gate — образец условности), adr-0008 (provenance — `.deploy-prev-image`/
|
||||
хук-откат). Прецедент ET-8. Будущее: ORCH-8 (петля уроков), ORCH-54 (полный авто).
|
||||
82
docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md
Normal file
82
docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# adr-0011: Job-reaper + проактивный реклейм merge-lease
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Статус | accepted |
|
||||
| Дата | 2026-06-07 |
|
||||
| Источник | ORCH-065 (BUG P0, блокер ORCH-54) |
|
||||
| Детально | `docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md` |
|
||||
|
||||
## Контекст
|
||||
|
||||
Единый инстанс с общей БД и очередью (`jobs`, `max_concurrency=1` для
|
||||
self-hosting). Финализация статуса job (`done`/`queued`/`failed`) происходит
|
||||
ТОЛЬКО в `launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляет строку `jobs` навсегда `running`. При
|
||||
`max_concurrency=1` одна такая зомби-строка блокирует claim всех job →
|
||||
**встаёт конвейер всех проектов**. Единственная защита — `requeue_running_jobs()`
|
||||
— работает ТОЛЬКО на старте процесса. Симметрично: merge-lease (ORCH-043,
|
||||
файл `.merge-lease-<repo>.json`) реклеймится лишь лениво по TTL при чужом
|
||||
`acquire`; liveness держателя по pid не проверяется → залипший lease блокирует
|
||||
чужие merge. Это последняя ручная точка автономного self-deploy (блокер ORCH-54);
|
||||
доказанные инциденты 07.06 — jobs 236/239/242/254.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **Job-reaper** — новый daemon-поток `src/job_reaper.py` (каркас `reconciler`:
|
||||
never-raise, `_stop`-Event, старт/стоп в `lifespan`, снимок в `/queue`,
|
||||
kill-switch). Работает **без рестарта** процесса. Liveness — трёхуровневая:
|
||||
Tier-1 мёртвый `jobs.pid` (новая колонка) после `reaper_dead_ticks` подряд
|
||||
тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running` — но только
|
||||
после finalization-grace `reaper_finalize_grace_s` (окно неоднозначно: живой
|
||||
monitor пишет exit_code ПЕРВЫМ, затем git push/PR/Plane-комментарии, поэтому
|
||||
живой финализирующий monitor НЕ реапится); Tier-3 backstop по потолку
|
||||
`reaper_max_running_s`. Действие — **claim-before-act**: для exit0 канонический
|
||||
QG оценивается read-only ПЕРЕД атомарным claim, затем claim `done` ПЕРВЫМ и
|
||||
только победитель claim выполняет `_try_advance_stage` (advance+enqueue) —
|
||||
проигравший не делает побочных эффектов (источник истины — QG, не «exit0»);
|
||||
гейт красный или exit≠0 / неизвестно → `attempts<max` → `queued`, иначе
|
||||
`failed`+Telegram. Атомарный reap-claim (`UPDATE ... WHERE id=? AND
|
||||
status='running'` + `rowcount`, как `claim_next_job`) исключает двойную
|
||||
обработку (совместимость со стартовым `requeue_running_jobs`).
|
||||
2. **Проактивный реклейм stale/dead lease** — функции в `merge_gate.py`
|
||||
(`pid_alive`, `reclaim_stale_lease`), вызываемые на старте (рядом с
|
||||
`requeue_running_jobs`) и периодически из тика reaper. Освобождение, если
|
||||
держатель **мёртв** (pid не жив) ИЛИ **просрочен** (TTL); живой держатель в
|
||||
пределах TTL — НЕ трогать. holder-aware, never-raise, условность как ORCH-43.
|
||||
3. **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
|
||||
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не
|
||||
повторяются (`branch_is_behind_main==False`); добавлен детерминированный
|
||||
never-raise guard `pr_already_merged` (читает состояние PR), консультируемый
|
||||
перед повторным merge → уже слит = no-op.
|
||||
4. **Схема БД** — `jobs.pid INTEGER` через идемпотентный `_ensure_column`
|
||||
(паттерн live-safe миграции). Больше ничего не меняется.
|
||||
|
||||
Kill-switch'и (`ORCH_*`): `reaper_enabled`, `reaper_interval_s`,
|
||||
`reaper_dead_ticks`, `reaper_max_running_s`, `reaper_finalize_grace_s`,
|
||||
`lease_reclaim_enabled`; переиспользуются `merge_lock_timeout_s`,
|
||||
`merge_gate_repos`. `false` → строго прежнее поведение.
|
||||
|
||||
## Альтернативы
|
||||
- Reaper внутри reconciler — отвергнуто (смешение stage- и jobs-уровней, общий
|
||||
kill-switch, хуже изоляция).
|
||||
- Только эвристика `agent_runs` без `jobs.pid` — отвергнуто как основной механизм
|
||||
(не ловит зомби, чей monitor умер до записи exit_code); оставлена как Tier-2/3.
|
||||
- БД-lock / внешний брокер очередей — вне объёма (single-node SQLite).
|
||||
- Форс `done` по факту exit0 — отвергнуто; выбран gate-driven advance.
|
||||
|
||||
## Последствия
|
||||
- (+) Зомби-job и залипший lease самовосстанавливаются без рестарта и без
|
||||
оператора; очередь общего инстанса не встаёт; снят технический блокер ORCH-54.
|
||||
- (+) Контракты неизменны (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука); одна колонка через проверенный idempotent-паттерн.
|
||||
- (−) pid-liveness валиден в предположении одного pid-namespace (агент —
|
||||
дочерний процесс оркестратора); закрыто backstop'ом по времени и TTL.
|
||||
- (−) streak-счётчик in-memory (сброс на рестарте; рестарт покрыт
|
||||
`requeue_running_jobs`).
|
||||
|
||||
## Связи
|
||||
- Базируется: adr-0002 (очередь), adr-0006 (merge-gate), adr-0007 (reconciler /
|
||||
self-deploy).
|
||||
- Разблокирует: ORCH-54.
|
||||
63
docs/architecture/adr/adr-0012-security-gate.md
Normal file
63
docs/architecture/adr/adr-0012-security-gate.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# adr-0012: Security-гейт — secret-scanning + dependency audit перед мержем
|
||||
|
||||
- **Статус:** proposed
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-022
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`
|
||||
|
||||
## Контекст
|
||||
Оркестратор автономен: `developer` пишет код без человека-фильтра. Перед слиянием ветки в
|
||||
`main` нет проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую
|
||||
зависимость (CVE). Для self-hosting один общий прод-инстанс обслуживает все проекты с общей
|
||||
БД — секрет/CVE через одну задачу попадает в прод всех (CLAUDE.md §self-hosting, §8). Фактический
|
||||
мерж PR в `main` делает `deployer` в начале стадии `deploy`.
|
||||
|
||||
## Решение
|
||||
Детерминированный (без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**,
|
||||
рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди
|
||||
edge-под-гейтов (ДО merge-gate). `STAGE_TRANSITIONS` не меняется; в `QG_CHECKS` добавлен
|
||||
`check_security_gate`. Паттерн — как у соседей: leaf-модуль `src/security_gate.py`
|
||||
(never-raise) + тонкая обёртка в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
|
||||
|
||||
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
|
||||
аллоулиста (`.gitleaks.toml`) → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
|
||||
не зависит от сети.
|
||||
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
|
||||
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
|
||||
громкий warning** (анти-петля; флаг `security_dep_audit_fail_closed` для строгого режима).
|
||||
- **ПЕРВЫМ на ребре, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки
|
||||
ДО rebase не «обвиняет» задачу в CVE, притащенной обновившимся `main` (анти-петля
|
||||
ORCH-061); до захвата merge-lease → при FAIL lease освобождать не нужно.
|
||||
- **Артефакт `17-security-report.md`** с YAML-frontmatter (`security_status`,
|
||||
`secrets_found`, `deps_blocking`, `deps_warning`, `deps_audit_degraded`); вердикт читается
|
||||
ТОЛЬКО из frontmatter (канон), negative-токен авторитетен; битый/нет → fail-closed.
|
||||
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
|
||||
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
|
||||
- **Условность (как ORCH-35/43/58):** `security_gate_enabled` + `security_gate_repos`; пусто
|
||||
→ реально только self-hosting (`orchestrator`), прочие репо — no-op pass.
|
||||
- **never-raise**, таймаут `security_scan_timeout_s`, гейт не деплоит/не рестартит прод.
|
||||
|
||||
## Альтернативы
|
||||
- **Вариант R (review-стадия):** diff может разойтись с мержем в `main`; merge-edge — последняя
|
||||
страховка. Отклонено.
|
||||
- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо
|
||||
выражаются статусом коммита; коуплинг с раннером. Отклонено для v1 (точка расширения).
|
||||
- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как в ORCH-043).
|
||||
Отклонено.
|
||||
- **fail-closed dep-audit / аудит после rebase:** ложные откаты → петля. Отклонено.
|
||||
- **Новая колонка retry в БД:** не нужна (переиспользуем `_developer_retry_count`).
|
||||
|
||||
## Последствия
|
||||
- Класс «тихо влитый секрет/CVE» закрыт: секреты — безусловно (offline), CVE — best-effort при
|
||||
доступности фида. Самоприменение CLAUDE.md §8 без человека.
|
||||
- Плата: ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); внешние инструменты
|
||||
(gitleaks в образе, pip-audit в зависимостях); время скана на каждом прогоне (ограничено
|
||||
таймаутом); v1 — Python-only (SAST/мульти-стек — follow-up WI).
|
||||
- Сквозное изменение (новый QG + edge-под-гейт) → `arch:major-change`; прод-деплой ORCH-022 —
|
||||
строго через staging-гейт (8501), без рестарта прод-контейнера.
|
||||
|
||||
## Связи
|
||||
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
|
||||
условность/never-raise/fail-closed), adr-0003 (условный гейт / `is_self_hosting_repo`),
|
||||
adr-0009 (анти-петля ложных FAIL, ORCH-061), ORCH-046 (дословный reason в `task_desc`),
|
||||
ORCH-9/15 (мульти-стек — будущая зависимость), ORCH-2 (worktree-изоляция).
|
||||
63
docs/architecture/adr/adr-0013-merge-verify-gate.md
Normal file
63
docs/architecture/adr/adr-0013-merge-verify-gate.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# adr-0013: Merge-в-main + пост-деплой верификация как условие `done` (фикс фантомного merge)
|
||||
|
||||
- **Статус:** accepted
|
||||
- **Дата:** 2026-06-08
|
||||
- **Задача:** ORCH-071 (CRITICAL bug)
|
||||
- **Детальный ADR:** `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`
|
||||
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`
|
||||
|
||||
## Контекст
|
||||
Для self-hosting репо `orchestrator` стадия `deploy` идёт детерминированным путём
|
||||
(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`), а LLM-агент
|
||||
`deployer` НЕ запускается. Фактический merge PR в `main` исторически делал **только**
|
||||
агент `deployer` → на self-hosting пути **нет шага merge-в-main вообще**. Detached
|
||||
host-деплой лишь retag'ает образ + рестартит 8500; `done` достигается по
|
||||
`deploy_status: SUCCESS` без верификации `main`. «Зелёный» деплой (образ из рабочей
|
||||
ветки) маскирует отсутствие merge → следующая задача срезает ветку от устаревшего `main`
|
||||
и теряет код предшественника. Накопительно потеряны ORCH-022/059/066/068. Вторичный
|
||||
фактор: Phase B рестартит прод → merge внутри живого процесса гонялся бы с рестартом
|
||||
(урок №3).
|
||||
|
||||
## Решение
|
||||
Детерминированный **merge-актор + пост-merge верификация** как **под-гейт ребра
|
||||
`deploy → done`**, врезанный в единственную функцию перехода `advance_stage` (симметрично
|
||||
edge-под-гейтам security/merge-gate/image-freshness). `STAGE_TRANSITIONS`,
|
||||
`check_deploy_status`/`_parse_deploy_status`, реестр `QG_CHECKS`, схема БД — **не меняются**.
|
||||
|
||||
- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и
|
||||
`next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`).
|
||||
Гейтит **ВСЕ** пути к `done` единообразно: `run_deploy_finalizer` (Phase C), reconciler
|
||||
F-1, job-reaper — все идут через `advance_stage`. Закрывает дыру: reconciler F-1 иначе
|
||||
протолкнул бы `done` в обход merge.
|
||||
- **Merge в Phase C (после рестарта), НЕ в Phase B.** Phase C finalizer —
|
||||
restart-surviving (reserved-job `deploy-finalizer`, claim воркером нового контейнера,
|
||||
re-drive reaper'ом). Merge физически строго ПОСЛЕ рестарта → рестарт его не убивает
|
||||
(G3 вторым вариантом — «шаг, переживающий рестарт»).
|
||||
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (no-op повтор, ORCH-065) →
|
||||
иначе Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Никогда push/force-push в
|
||||
`main`. never-raise.
|
||||
- **Верификатор `merge_gate.verify_merged_to_main`** — `PR.merged==true` ИЛИ
|
||||
`git merge-base --is-ancestor <validated_sha> origin/main`. never-raise → `False`
|
||||
(«не подтверждено»).
|
||||
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
|
||||
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged
|
||||
есть инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено →
|
||||
штатный `deploy → done` (терминал-sync / post-deploy monitor как сегодня) +
|
||||
`merged_to_main: true` во frontmatter `14-deploy-log.md` (наблюдаемость, `deploy_status:`
|
||||
нетронут).
|
||||
- **Идемпотентность (INV-5):** `pr_already_merged` перед merge; verify зелёный для
|
||||
уже-слитого PR; повтор без дубль-merge/ложного отката.
|
||||
- **Условность (как ORCH-35/43/58):** `merge_verify_enabled` (kill-switch, дефолт `true`) +
|
||||
`merge_verify_repos` (пусто → только self-hosting). Non-self репо — no-op, merge остаётся
|
||||
за агентом `deployer`.
|
||||
|
||||
## Инварианты
|
||||
never-raise на verify/merge (ошибка → alert, не падение конвейера); не рестартить/не ронять
|
||||
прод 8500; ручной approve прод-деплоя сохранён (`Confirm Deploy`, ORCH-059); только PR-merge
|
||||
API Gitea; restart-safe (sentinel + jobs, без миграции БД).
|
||||
|
||||
## Последствия
|
||||
Невозможно «`done` + прод задеплоен, а PR `open`». Минусы: при недоступной Gitea verify
|
||||
консервативно `False` → возможен ложный HOLD+alert (снимается повтором; fail-closed для
|
||||
`done` приоритетен); HOLD требует ручного вмешательства. Диагностика фантома — runbook
|
||||
`docs/operations/PHANTOM_MERGE_RUNBOOK.md` (G4).
|
||||
@@ -111,12 +111,12 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
|
||||
|
||||
Вместо ~15 отдельных сообщений на задачу оркестратор держит **ОДНУ** live-карточку на задачу (`update_task_tracker`), которая обновляется на каждом переходе стадии. Текст рендерится статически из БД (`render_task_tracker`: стадии, токены, стоимость, BRD-подтверждение, итоги). Карточка всегда тихая (`disable_notification=True`); отдельные пинги шлют только `notify_approve_requested` / `notify_error`. `message_id` хранится в `tasks.tracker_message_id`; helpers `get_tracker_message_id` / `set_tracker_message_id`. Контракт всего компонента — **never raises**.
|
||||
|
||||
**Режимы (ORCH-042, `ORCH_TRACKER_MODE` → `Settings.tracker_mode`).** Резолвится в `update_task_tracker` (case-insensitive, trim); всё, что ≠ `"bump"` (включая пустое/мусор/None), трактуется как `edit` → нулевая регрессия и безопасный фолбэк. Инвариант «одна карточка на задачу» сохраняется в обоих режимах.
|
||||
**Режимы (ORCH-042, `ORCH_TRACKER_MODE` → `Settings.tracker_mode`; дефолт переключён `edit → bump` в ORCH-067).** Резолвится в `update_task_tracker` (case-insensitive, trim); всё, что ≠ `"bump"` (включая пустое/мусор/None), трактуется как `edit` → безопасный фолбэк. Инвариант «одна карточка на задачу» сохраняется в обоих режимах.
|
||||
|
||||
| Режим | Поведение при обновлении |
|
||||
|-------|--------------------------|
|
||||
| `edit` (дефолт) | первый вызов → `send_telegram` (тихо) + сохранение `message_id`; далее → `edit_telegram` на сохранённый id. Новое сообщение шлётся ТОЛЬКО при `EDIT_GONE` (удалено/старше 48ч/невалидный id). `EDIT_NOT_MODIFIED` / `EDIT_FAILED` → нового сообщения нет (анти-дубль). |
|
||||
| `bump` | карточка пересоздаётся внизу чата: best-effort `delete_telegram(старый_id)` → `send_telegram(text, disable_notification=True)` → `set_tracker_message_id(new_id)` **только** при успешном send (`new_mid is not None`). За один вызов — не более одного нового сообщения. |
|
||||
| `bump` (дефолт, ORCH-067) | карточка пересоздаётся внизу чата: best-effort `delete_telegram(старый_id)` → `send_telegram(text, disable_notification=True)` → `set_tracker_message_id(new_id)` **только** при успешном send (`new_mid is not None`). За один вызов — не более одного нового сообщения. Живая карточка всегда «догоняет» переписку. |
|
||||
| `edit` | первый вызов → `send_telegram` (тихо) + сохранение `message_id`; далее → `edit_telegram` на сохранённый id. Новое сообщение шлётся ТОЛЬКО при `EDIT_GONE` (удалено/старше 48ч/невалидный id). `EDIT_NOT_MODIFIED` / `EDIT_FAILED` → нового сообщения нет (анти-дубль). |
|
||||
|
||||
**`delete_telegram(message_id) -> bool`** (low-level, never raises). Семантика возврата — «исчезло ли старое сообщение»:
|
||||
- `ok:true` → `True`;
|
||||
@@ -128,6 +128,12 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash
|
||||
|
||||
**Текст карточки (оба режима, ORCH-042):** метка `Подтверждение BRD` (была «Ревью БРД»); после прохождения approve-gate строка BRD начинается с ✅ (ветка ожидания сохраняет ⏸️/⏳); русские display-labels стадий (`Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение`); финальная строка `📦 Внедрено` (было `deployed`). Меняются только отображаемые строки — ключи стадий и имена агентов (завязаны на `_STAGE_ACTIVE_AGENT`, `last_done`, БД) не трогаются.
|
||||
|
||||
**Строка 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`.
|
||||
- **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`).
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
@@ -326,6 +332,7 @@ webhook (plane/gitea) background thread (queue_worker)
|
||||
| `status` | `queued` → `running` → `done` \| `failed` |
|
||||
| `attempts` / `max_attempts` | счётчик попыток (инкремент при claim) / лимит ретраев (default 2) |
|
||||
| `run_id` | FK на `agent_runs.id` после старта |
|
||||
| `pid` | (ORCH-065) pid агентского процесса (`proc.pid` из `_spawn`); liveness-сигнал для job-reaper. Добавляется `_ensure_column` (idempotent) |
|
||||
| `task_content` | ТЗ, которое пишется в task-файл агента |
|
||||
| `error` | последняя ошибка |
|
||||
|
||||
@@ -343,6 +350,36 @@ status='queued'` и проверяет `rowcount`. При гонке двух т
|
||||
jobs со статусом `running` (воркер умёр на рестарте) → возвращаются в `queued`.
|
||||
Потом стартует воркер; на shutdown — `worker.stop()` (Event.set + join).
|
||||
|
||||
### Job-reaper (ORCH-065, рестарт НЕ требуется)
|
||||
|
||||
`requeue_running_jobs()` спасает ТОЛЬКО на старте процесса. Зомби-job, возникший
|
||||
**без** рестарта (умер monitor-поток/дочерний процесс, а сервис жив), оставался
|
||||
`running` навсегда и при `max_concurrency=1` блокировал всю очередь. Фоновый
|
||||
daemon-поток `src/job_reaper.py` (каркас `reconciler`) периодически
|
||||
(`reaper_interval_s`) сканирует `running`-jobs и реапит «мёртвые»:
|
||||
- **Tier-1** — `jobs.pid` мёртв (`os.kill(pid,0)`→`ProcessLookupError`) на
|
||||
протяжении `reaper_dead_ticks` подряд тиков (анти-ложноположительность);
|
||||
- **Tier-2** — у `agent_runs[run_id]` записан `exit_code`, а `jobs.status` ещё
|
||||
`running`. Окно неоднозначно: живой monitor пишет `exit_code` ПЕРВЫМ, затем
|
||||
git push/PR/Plane-комментарии (секунды-десятки секунд) и лишь потом
|
||||
`_finalize_job`; pid агента к этому моменту мёртв в обоих случаях. Поэтому
|
||||
Tier-2 реапит только после finalization-grace `reaper_finalize_grace_s`
|
||||
(`finished_age_s >= grace`) — живой финализирующий monitor НЕ реапится;
|
||||
- **Tier-3** — backstop: job висит `running` дольше `reaper_max_running_s`.
|
||||
|
||||
Реап атомарен (`UPDATE jobs SET ... WHERE id=? AND status='running'` + `rowcount`,
|
||||
как `claim_next_job`) → совместим со стартовым `requeue_running_jobs` без двойной
|
||||
обработки. Действие — **claim-before-act**: для exit0 канонический QG оценивается
|
||||
read-only ПЕРЕД атомарным claim, затем claim `done` ПЕРВЫМ и только победитель
|
||||
claim делает `_try_advance_stage` (advance+enqueue) — проигравший (поздний monitor
|
||||
/ стартовый requeue) не выполняет побочных эффектов (нет дубль-advance/-enqueue);
|
||||
источник истины — QG, не «exit0»; гейт красный или exit≠0/неизвестно →
|
||||
`attempts<max`→`queued`, иначе `failed`+Telegram. Тот же поток на старте и
|
||||
периодически делает проактивный реклейм stale/dead merge-lease (`merge_gate.py`:
|
||||
`pid_alive`/`reclaim_stale_lease`). never-raise; kill-switch `ORCH_REAPER_ENABLED`
|
||||
/ `ORCH_LEASE_RECLAIM_ENABLED`; снимок в `GET /queue` (блок `reaper`). Подробнее —
|
||||
adr-0011.
|
||||
|
||||
### Конфиг
|
||||
|
||||
- `ORCH_MAX_CONCURRENCY` (default 1) — лимит параллельных jobs.
|
||||
|
||||
78
docs/history/LESSONS_2026-06-07_autonomy-closure.md
Normal file
78
docs/history/LESSONS_2026-06-07_autonomy-closure.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Lessons Learned — 2026-06-07: замыкание автономности self-deploy (5 задач в прод)
|
||||
|
||||
## Итог
|
||||
За одну сессию закрыты в прод **5 задач**, завершающих автономный self-deploy эпика ORCH-54:
|
||||
|
||||
| Задача | Что | Прод-коммит |
|
||||
|--------|-----|-------------|
|
||||
| ORCH-58 | provenance retag-guard (свежесть staging-образа перед BUILD-ONCE) | 094b5e2 |
|
||||
| ORCH-60 | reconciler не трогает escalated/Blocked/Needs-Input | d4c6cc0 |
|
||||
| ORCH-61 | фикс петли deploy-staging (staging_verdict: waive sandbox-infra FAILs C9a/C9b) | e18947d |
|
||||
| ORCH-21 | post-deploy мониторинг прода + auto-rollback (self-hosting=alert-only) | f85e449 |
|
||||
| ORCH-65 | job-reaper + stale merge-lease reclaim + idempotent merge | bb03350 |
|
||||
|
||||
**Главное:** после ORCH-60/61 конвейер впервые провёз задачи (ORCH-21/65) через deploy-staging
|
||||
**автономно** без отката; после ORCH-65 (job-reaper в проде) зомби-job и зависшие merge-lease
|
||||
лечатся сами. Последняя ручная точка автономного деплоя закрыта.
|
||||
|
||||
---
|
||||
|
||||
## Класс багов: «процесс умер — ресурс захвачен навсегда» (ORCH-65)
|
||||
Три связанных отказа, все воспроизвелись на ORCH-58/60/61/21:
|
||||
- **zombie jobs:** агент завершился/умер, строка jobs осталась running. requeue_running_jobs()
|
||||
спасает только на старте процесса; зомби без рестарта не лечился → при concurrency=1 встаёт
|
||||
конвейер ВСЕХ проектов. (jobs 236/239/242/254/265 — все зомби за сессию.)
|
||||
- **stale merge-lease:** merge-gate берёт .merge-lease-<repo>.json, делает rebase+re-test green,
|
||||
а на финальном merge процесс умирает с зажатым lease → merge не докатывается.
|
||||
- **неидемпотентный merge:** re-drive повторно пытается слить уже слитый PR.
|
||||
Фикс: фоновый job_reaper (паттерн reconciler, dead_ticks streak + мёртвый pid + exit_code,
|
||||
атомарный reap-claim, never-raise, kill-switch, снимок в /queue) + проактивный lease-reclaim
|
||||
по pid + guard pr_already_merged ПЕРЕД merge.
|
||||
|
||||
## Петля deploy-staging (ORCH-61) — ДВЕ причины
|
||||
1. ложный check_staging_status FAILED: staging_check падает на C9a/C9b (sandbox e2e branch +
|
||||
analyst-job-in-queue), т.к. bot-токены SANDBOX-проекта не настроены — НЕ регресс кода.
|
||||
2. no-changes для action-стадий (деплой = рестарт/retag, не правка → коммитить нечего).
|
||||
Фикс: staging_verdict waive sandbox-infra-only FAILs.
|
||||
|
||||
## Инфра-каскад от переполненного диска (инцидент дня)
|
||||
- Частые build-once/--build-staging пересборки за день забили docker build cache до 11 ГБ →
|
||||
диск 100% → CI red (No space left).
|
||||
- ДАЖЕ после чистки диска Gitea осталась в сломанном состоянии: внутренняя queue
|
||||
(/data/gitea/queues/common/*.log) залипла → post-receive hook 500 → actions tasks НЕ
|
||||
создаются, CI не триггерится вовсе (статус пустой, не failure). runner при этом online+idle.
|
||||
- Лечение: docker builder prune -af + рестарт Gitea (queue распускается → CI ожил).
|
||||
|
||||
---
|
||||
|
||||
## Уроки
|
||||
1. **Self-hosting safety (сквозной принцип):** прод-орк обслуживает ВСЕ проекты. Нельзя авто-
|
||||
откатывать/рестартить self в рамках задачи; нельзя пушить main. ORCH-21 post-deploy для
|
||||
self-hosting = alert-only, авто-rollback только для не-self репо.
|
||||
2. **TDD без доводки (повтор ORCH-58 и ORCH-65 v1):** тесты есть, реализация/wiring не
|
||||
подключены к боевому пути → мёртвый код + врущая дока. Reviewer обязан грепать вызовы из
|
||||
прод-кода, не только наличие функции.
|
||||
3. **Concurrency-баги ловятся итеративно:** ORCH-65 3 прохода reviewer (мёртвый guard → race
|
||||
condition side-effects-before-claim → approve) — каждый раз НОВЫЙ реальный дефект, не
|
||||
зацикливание. Atomic-claim ДО side-effects — обязательное правило.
|
||||
4. **При красном CI + зелёных локальных тестах — ПЕРВЫМ делом df -h / и docker system df**,
|
||||
не копаться в коде. После disk-full обязателен рестарт Gitea (queue залипает).
|
||||
5. **Bootstrap-разрыв:** задача про автономность деплоя не может задеплоить себя автономно,
|
||||
пока её механизм не в проде. Последний прод-деплой каждого такого фикса — вручную.
|
||||
6. **Перед прод-retag (build-once SOURCE_IMAGE=staging):** проверить revision-label staging-
|
||||
образа == целевой main HEAD, иначе guard fail-closed (by design). Если != → пересобрать
|
||||
--build-staging GIT_SHA=<main HEAD>.
|
||||
|
||||
## Ручная доводка прод-deploy (схема до ORCH-65 в проде)
|
||||
cancel zombie job → park task In Progress → merge PR (Gitea pulls/{n}/merge Do=merge, CI green)
|
||||
→ --build-staging GIT_SHA=<main HEAD> (проставит label) → rollback-снимок → --deploy с
|
||||
EXPECTED_REVISION=<sha> (guard сверит → retag → health 200) → Plane Done + UPDATE tasks stage=done.
|
||||
|
||||
## Follow-up (Backlog)
|
||||
- ORCH-62: авто-prune docker build cache (cron/daemon.json defaultKeepStorage).
|
||||
- ORCH-63: мониторинг диска mva154 + алерт >85%.
|
||||
- ORCH-64: починить NTP/часы mva154 (ушли ~+3ч от UTC).
|
||||
|
||||
## Осталось в эпике ORCH-54
|
||||
ORCH-22 (security-гейт), ORCH-59 (Confirm Deploy статус), ORCH-23 (budget circuit-breaker),
|
||||
P2: ORCH-57, ORCH-51.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Lessons Learned — 2026-06-08: статус `Confirm Deploy` не триггерит Phase B (мёртвый триггер)
|
||||
|
||||
## Контекст
|
||||
ORCH-066 ввела новую статусную модель Plane, включая человекочитаемый статус **`Confirm Deploy`** для прод-деплойного approve-gate (self-deploy Phase B). Орк сам выставляет задачу в `Awaiting Deploy` / `Confirm Deploy` через `set_issue_awaiting_deploy()` и т.п.
|
||||
|
||||
## Инцидент (2026-06-08, первый реальный прод-self-deploy — ORCH-068)
|
||||
Слава нажал статус **`Confirm Deploy`** в Plane, ожидая запуск прод-деплоя. Орк ответил `no pipeline action` и НИЧЕГО не запустил. Прод-деплой стартовал только после ручного перевода в **`Approved`**.
|
||||
|
||||
## Root cause
|
||||
Диспетчер статусов `handle_issue_status` (`src/webhooks/plane.py` ~158-166) слушает РОВНО три состояния:
|
||||
```python
|
||||
if new_state == proj_states["to_analyse"]: await handle_status_start(...)
|
||||
elif new_state == proj_states["approved"]: await handle_verdict(..., approved=True)
|
||||
elif new_state == proj_states["rejected"]: await handle_verdict(..., approved=False)
|
||||
else: logger.info("... no pipeline action")
|
||||
```
|
||||
Phase B (прод-деплой) триггерится в `_try_advance_stage` (`src/stage_engine.py` ~215-224) при `current_stage == "deploy" and finished_agent is None` — то есть ТОЛЬКО когда пришёл вебхук `Approved`. Статус `Confirm Deploy` в эту тройку НЕ входит → ветка `else` → no-op.
|
||||
|
||||
**ORCH-066 добавила статус как МЕТКУ (запись), но не подключила обратный путь (чтение/триггер).** Классическая дыра: протестировали, что орк правильно СТАВИТ статус, но не протестировали, что нажатие этого статуса человеком РЕАЛЬНО запускает действие.
|
||||
|
||||
## Почему не поймали тестирование/ревью
|
||||
1. **Не в scope ORCH-068.** ORCH-068 чинит reconciler (BRD §6 N1-N3 явно: не трогать диспетчер статусов / Phase B). Тестер прогнал TC-01..13 — все про reconciler/terminal-статусы. Ревьюер смотрел diff reconciler.py/plane_sync.py. Корректно — это дефект ORCH-066, не 068.
|
||||
2. **Дыра ORCH-066.** Её тесты, видимо, проверяли запись статусов, а не обратный триггер.
|
||||
3. **Staging не покрывает прод-путь.** Phase A (staging-деплой) автоматический, ручной `Confirm Deploy` живёт ТОЛЬКО на прод-пути, который на staging не гоняется. Поэтому всплыло лишь на первом реальном прод-деплое.
|
||||
|
||||
## Уроки
|
||||
1. **Тестировать обратный путь статусов, не только запись.** Для каждого статуса, который человек может нажать, нужен тест «нажатие → ожидаемое pipeline-действие». Запись (орк ставит статус) и чтение (орк реагирует на статус) — два разных контракта.
|
||||
2. **Прод-only пути (ручной Confirm Deploy) нуждаются в явном тесте/чеклисте.** Staging их не ловит by design. Любой approve-gate, доступный человеку, обязан иметь регресс-тест на триггер.
|
||||
3. **Новый статус = подключить В ОБЕ стороны.** При добавлении статуса в модель — сразу проверить, что диспетчер `handle_issue_status` его слушает (если он actionable), а не только что орк его выставляет.
|
||||
4. **UX-консистентность:** статус, названный действием («Confirm Deploy»), обязан выполнять это действие. Иначе оператор жмёт интуитивную кнопку, а система молчит → потеря доверия к автономности.
|
||||
|
||||
## Фикс
|
||||
Заведена ORCH-070: подключить `Confirm Deploy` (или его actionable-эквивалент) к триггеру Phase B в `handle_issue_status`, + регресс-тест на обратный путь статусов прод-деплоя. Source-of-truth и существующий `Approved`-путь не ломать (обратная совместимость).
|
||||
47
docs/history/LESSONS_2026-06-08_phantom-merge.md
Normal file
47
docs/history/LESSONS_2026-06-08_phantom-merge.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Lessons Learned — 2026-06-08: «Фантомный merge» — прод деплоится, но код не сливается в main
|
||||
|
||||
## Severity: CRITICAL (потеря целостности main, накопительная потеря кода между задачами)
|
||||
|
||||
## Резюме
|
||||
Self-deploy (Phase B) собирал прод-образ из ВЕТКИ задачи и рапортовал `finalize SUCCESS` + `post-deploy HEALTHY`, но git-merge ветки в `main` НЕ происходил. PR оставался `open`. Следующая задача срезала свою ветку от устаревшего main → теряла код незалитых предшественников. Накопительно потеряны в main: **ORCH-022, ORCH-059, ORCH-066, ORCH-068** (PR#67/68/69/70 — все open, merged=False). Последний реально слитый — ORCH-065 (PR#66).
|
||||
|
||||
## Как обнаружено
|
||||
Симптом: ORCH-067 переведён в `To Analyse`, но конвейер не стартовал (`no pipeline action`). Причина — прод слушал старый триггер `in_progress`, а не `to_analyse` (ORCH-066). При разборе выяснилось: код ORCH-066 не в проде, хотя он «деплоился».
|
||||
|
||||
Решающее наблюдение оператора (Слава): «спам ET-002 начался СРАЗУ после деплоя 66 → значит код деплоился». Это вскрыло механизм: код 66 БЫЛ в проде 22:17–05:32, потом стёрт деплоем 068 (срезан от старого main без 66).
|
||||
|
||||
## Доказательная база (как подтверждали — воспроизводимый метод)
|
||||
1. **PR-статус (Gitea API):** PR#67(022)/68(059)/69(066)/70(068) = open, merged=False. PR#66(065) = merged=True (последний честный).
|
||||
2. **md5-сверка файлов прод vs origin/main vs ветка:**
|
||||
- `src/reconciler.py`, `src/plane_sync.py`: prod md5 == ветка ORCH-068 != main → прод = снимок ветки 068, НЕ main.
|
||||
- `src/webhooks/plane.py`: prod == main == ветка-068 (ветка 068 этот файл не трогала → видна старая база без to_analyse).
|
||||
3. **git merge-base:** ветка ORCH-068 срезана от `bb03350` (ORCH-065), не от кода 066. История ветки-068 по 066 содержит только `docs staging`, кода (`to_analyse`) нет.
|
||||
4. **Таймлайн логов:** деплой 22:17 (ветка-066, сломанный reconciler) → спам ET-002 начался; деплой 05:32 (ветка-068, база 065 без 66) → спам прекратился (0 после 05:33). Подтверждает: прод-образ = снимок ВЕТКИ, меняется при каждом деплое, теряет незалитое.
|
||||
|
||||
## Root cause (гипотеза → нужен код-аудит self_deploy/merge_gate)
|
||||
Self-deploy Phase B инициирует прод-деплой из worktree ветки (BUILD-ONCE из validated commit). Шаг git-merge ветки в main:
|
||||
- ЛИБО не вызывается на self-hosting пути (Phase B уходит в detached host-процесс, finalizer пишет SUCCESS-маркеры, но merge отдельно и молча скипается/падает),
|
||||
- ЛИБО регресс фикса ORCH-065 (idempotent merge / merge-lease reclaim): guard `pr_already_merged` или lease-reclaim ошибочно считает PR уже слитым / не докатывает merge после рестарта контейнера (а Phase B ИМЕННО рестартит контейнер → процесс, державший merge-lease, умирает до завершения merge).
|
||||
Симптоматически ORCH-065 был последним успешным merge — деградация началась СРАЗУ после него или из-за взаимодействия его механики с self-deploy-рестартом.
|
||||
|
||||
## Почему конвейер не заметил
|
||||
- `finalize SUCCESS` и `post-deploy HEALTHY` маркеры пишутся НЕЗАВИСИМО от факта merge. Пайплайн считает задачу done по этим маркерам, git-состояние main не верифицируется.
|
||||
- Прод здоров (образ из ветки рабочий) → health-check зелёный → нет сигнала о проблеме.
|
||||
- Дыра видна только при сравнении main с прод ИЛИ когда следующая задача теряет код предыдущей (что и случилось с 67).
|
||||
|
||||
## Уроки
|
||||
1. **Деплой ОБЯЗАН верифицировать, что код реально в main ПОСЛЕ деплоя.** finalize SUCCESS без проверки `git merge-base origin/main == deployed_commit` (или PR.merged==true) — фальшивый зелёный. Добавить post-merge верификацию: deployed SHA должен быть предком origin/main.
|
||||
2. **Маркер «deployed» != «merged».** Нельзя считать задачу завершённой по staging/post-deploy-маркерам, если PR не закрыт merge. Гейт: задача → done ТОЛЬКО при PR.merged==true.
|
||||
3. **Self-deploy рестартит контейнер → любой держатель merge-lease/незавершённый git-шаг умирает.** Merge ДОЛЖЕН завершиться и быть подтверждён ДО рестарта прод-контейнера, либо merge выносится в шаг, переживающий рестарт (как requeue_running_jobs, но для merge-в-main).
|
||||
4. **Срез ветки от main делает целостность main критичной.** Если main отстаёт — каждая новая задача наследует дыру. main = единственный источник для новых веток, его рассинхрон с прод = накопительная потеря.
|
||||
5. **Метод диагностики (сохранить как runbook):** при подозрении на рассинхрон — (a) Gitea API PR list merged-флаги, (b) md5 prod-файлов vs `git show origin/main:<file>`, (c) merge-base ветки vs main, (d) таймлайн деплой-логов. Эти 4 проверки однозначно локализуют фантом.
|
||||
|
||||
## Действия
|
||||
- Восстановление main: интеграционная ветка `integ/restore-main-2026-06-08` — последовательный merge 022→059→066→068 (docs union-resolved, reconciler-конфликт 066⊕068 разрешён: каркас 068 livelock-fix + триггер to_analyse 066), полный pytest, затем merge в main + передеплой.
|
||||
- Заведён критбаг ORCH-071: «фантомный merge — self-deploy без верификации merge в main» (root-fix: post-deploy verify + done-гейт по PR.merged + merge до рестарта).
|
||||
- ORCH-070 (Confirm Deploy trigger) частично ДУБЛИРУЕТ ORCH-059 (handle_confirm_deploy уже написан в 059) — после долива 059 пересмотреть scope 070 (остаётся только display-слой статусов Monitoring after Deploy).
|
||||
|
||||
## Связанные
|
||||
- ORCH-065 (последний честный merge; подозрение на регресс его merge-механики)
|
||||
- ORCH-066/068 (потерянный код), ORCH-059 (Confirm Deploy trigger, тоже потерян)
|
||||
- Урок 2026-06-08 confirm-deploy-deadtrigger (симптом того же корня)
|
||||
125
docs/operations/PHANTOM_MERGE_RUNBOOK.md
Normal file
125
docs/operations/PHANTOM_MERGE_RUNBOOK.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Runbook — диагностика «фантомного merge» (ORCH-071)
|
||||
|
||||
> **Когда применять.** Задача дошла до `done` (или прод задеплоен «зелёным»), но есть
|
||||
> подозрение, что её ветка **не влита в `main`** — следующая задача срежет ветку от
|
||||
> устаревшего `main` и потеряет код предшественника (постмортем
|
||||
> `docs/history/LESSONS_2026-06-08_phantom-merge.md`). Этот runbook даёт 4 проверки
|
||||
> для **однозначной локализации** фантома.
|
||||
|
||||
С ORCH-071 такой исход блокируется автоматически: под-гейт `deploy → done`
|
||||
(`stage_engine._handle_merge_verify`) сначала **детерминированно вливает PR**
|
||||
(`merge_gate.merge_pr`, Gitea PR-merge API), затем **верифицирует merge**
|
||||
(`merge_gate.verify_merged_to_main`) и НЕ пускает задачу в `done`, пока merge не
|
||||
подтверждён (alert + HOLD). Этот runbook — для ручной перепроверки/инцидентов
|
||||
(в т.ч. при выключенном kill-switch `ORCH_MERGE_VERIFY_ENABLED=false`).
|
||||
|
||||
Подставьте значения:
|
||||
|
||||
```bash
|
||||
OWNER=admin # settings.gitea_owner
|
||||
REPO=orchestrator # репозиторий
|
||||
BRANCH=feature/ORCH-071-slug # ветка задачи
|
||||
GITEA=http://localhost:3000 # settings.gitea_url
|
||||
TOKEN=<gitea_token> # settings.gitea_token
|
||||
FILE=src/stage_engine.py # любой файл, гарантированно изменённый задачей
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Проверка 1 — Gitea API: список PR + флаги `merged`
|
||||
|
||||
Показывает, считает ли сам Gitea PR влитым.
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: token $TOKEN" \
|
||||
"$GITEA/api/v1/repos/$OWNER/$REPO/pulls?state=all" \
|
||||
| python3 -c 'import sys,json; \
|
||||
[print(p["number"], p["state"], "merged="+str(p.get("merged")), p["head"]["ref"]) \
|
||||
for p in json.load(sys.stdin)]'
|
||||
```
|
||||
|
||||
* **Фантом НЕ подтверждён (всё хорошо):** строка ветки `$BRANCH` имеет `merged=True`.
|
||||
* **Фантом подтверждён (по этому критерию):** PR ветки `state=open` / `merged=False`
|
||||
(или PR отсутствует), при том что задача в `done` / прод задеплоен.
|
||||
|
||||
---
|
||||
|
||||
## Проверка 2 — md5 прод-файлов vs `git show origin/main:<file>`
|
||||
|
||||
Сверяет содержимое файла на проде с тем, что лежит в `origin/main`.
|
||||
|
||||
```bash
|
||||
# в прод-контейнере (или через docker exec orchestrator):
|
||||
md5sum "/app/$FILE"
|
||||
|
||||
# содержимое того же файла из origin/main (на хосте, в клоне репо):
|
||||
git -C /home/slin/repos/$REPO fetch origin main -q
|
||||
git -C /home/slin/repos/$REPO show "origin/main:$FILE" | md5sum
|
||||
```
|
||||
|
||||
* **Совпало:** прод соответствует `main` (фантома нет ИЛИ задача не меняла этот файл —
|
||||
возьмите файл из проверки 3/diff'а ветки).
|
||||
* **Разошлось:** прод собран из ветки, а `main` его не получил → косвенный признак фантома.
|
||||
|
||||
---
|
||||
|
||||
## Проверка 3 — `git merge-base` ветки vs `main`
|
||||
|
||||
Главный детерминированный критерий: является ли HEAD ветки предком `origin/main`.
|
||||
|
||||
```bash
|
||||
git -C /home/slin/repos/$REPO fetch origin -q
|
||||
SHA=$(git -C /home/slin/repos/$REPO rev-parse "origin/$BRANCH")
|
||||
git -C /home/slin/repos/$REPO merge-base --is-ancestor "$SHA" origin/main \
|
||||
&& echo "MERGED: ветка влита в main" \
|
||||
|| echo "NOT MERGED: ветка НЕ предок origin/main (ФАНТОМ)"
|
||||
```
|
||||
|
||||
Это ровно та проверка, что выполняет `merge_gate.verify_merged_to_main` (rc=0 → влито).
|
||||
|
||||
* **`MERGED`:** фантома нет.
|
||||
* **`NOT MERGED`:** фантом подтверждён — `main` не содержит коммитов задачи.
|
||||
|
||||
---
|
||||
|
||||
## Проверка 4 — таймлайн деплой-логов
|
||||
|
||||
Восстанавливает порядок событий: был ли merge до/после деплоя, и был ли он вообще.
|
||||
|
||||
```bash
|
||||
# Вердикт деплоя + новое поле merge-верификации (ORCH-071):
|
||||
git -C /home/slin/repos/$REPO show "origin/$BRANCH:docs/work-items/<WI>/14-deploy-log.md" \
|
||||
| sed -n '1,12p' # frontmatter: deploy_status:, merged_to_main:
|
||||
|
||||
# Наблюдаемость под-гейта в живом сервисе:
|
||||
curl -s "$GITEA_HEALTH/queue" | python3 -c \
|
||||
'import sys,json; print(json.load(sys.stdin)["merge_verify"])'
|
||||
# -> {"enabled":..., "merge_verified_total":..., "not_merged_alerts_total":..., "last_alert_wi":...}
|
||||
|
||||
# Журнал хоста по деплою (sentinel-каталог задачи):
|
||||
ls -la /home/slin/repos/.deploy-state-$REPO/<WI>/
|
||||
cat /home/slin/repos/.deploy-state-$REPO/<WI>/hook.log
|
||||
```
|
||||
|
||||
* `deploy_status: SUCCESS` + `merged_to_main: false` → деплой прошёл, merge — нет
|
||||
(это и есть класс ORCH-071; задача должна быть удержана на `deploy`, не `done`).
|
||||
* `not_merged_alerts_total` растёт / `last_alert_wi == <WI>` → под-гейт уже поднял alert.
|
||||
|
||||
---
|
||||
|
||||
## Критерий «фантом подтверждён»
|
||||
|
||||
Фантомный merge считается **подтверждённым**, если выполняется ХОТЯ БЫ ОДНО из:
|
||||
|
||||
1. Проверка 1: PR ветки `state=open` / `merged=False` (или PR нет), а задача в `done`.
|
||||
2. Проверка 3: `merge-base --is-ancestor` вернул **NOT MERGED** (HEAD ветки не предок `origin/main`).
|
||||
3. Проверка 4: `14-deploy-log.md` имеет `deploy_status: SUCCESS` при `merged_to_main: false`.
|
||||
|
||||
Проверка 2 — вспомогательная (зависит от того, менял ли файл задачей), используется
|
||||
для подтверждения проверок 1/3.
|
||||
|
||||
### Что делать при подтверждённом фантоме
|
||||
|
||||
1. **Влить PR вручную** через Gitea (PR-merge API / UI) — НИКОГДА не `git push`/`--force` в `main` (INV-4).
|
||||
2. Повторить approve задачи (re-drive) — под-гейт переоценит: merge подтвердится → задача уйдёт в `done`.
|
||||
3. Если фантом случился при выключенном kill-switch — включить `ORCH_MERGE_VERIFY_ENABLED=true`.
|
||||
7
docs/work-items/ORCH-021/00-business-request.md
Normal file
7
docs/work-items/ORCH-021/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: [★ высокий] Post-deploy мониторинг прода + авто-rollback при деградации
|
||||
|
||||
Work Item ID: ORCH-021
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
88
docs/work-items/ORCH-021/01-brd.md
Normal file
88
docs/work-items/ORCH-021/01-brd.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# BRD — ORCH-021: Post-deploy мониторинг прода + авто-rollback при деградации
|
||||
|
||||
Work Item: ORCH-021
|
||||
Приоритет: высокий (★)
|
||||
Источник: предложение Стрим, одобрено Славой (2026-06-04)
|
||||
Стадия: analysis
|
||||
|
||||
## 1. Проблема (Why)
|
||||
|
||||
Сейчас конвейер заканчивается на `deploy → done`: как только `check_deploy_status`
|
||||
видит `deploy_status: SUCCESS`, задача закрывается и оркестратор **забывает про прод**.
|
||||
«Успех» деплоя сегодня означает только то, что health-check в момент рестарта
|
||||
прошёл (10×6с в `scripts/orchestrator-deploy-hook.sh`) — узкое окно ~60 секунд.
|
||||
|
||||
**Прямой урок ET-8:** деплой отрапортовал SUCCESS, а на проде фича не работала.
|
||||
Класс инцидентов — «зелёный деплой, красный прод»:
|
||||
- деградация проявляется через минуты, а не в первые 60с (прогрев кэшей, фоновые
|
||||
миграции, отложенные запросы, утечки, рост 5xx под реальным трафиком);
|
||||
- health-эндпоинт отвечает `200 ok`, но ключевая функциональность сломана;
|
||||
- регресс виден только под боевым трафиком, которого нет в момент рестарта.
|
||||
|
||||
После закрытия задачи никакого пригляда за продом нет — деградацию замечает человек
|
||||
постфактум. Для self-hosting это особенно опасно: сломанный прод-орк (8500) обслуживает
|
||||
ВСЕ проекты (enduro-trails) из общего инстанса.
|
||||
|
||||
## 2. Цель (What)
|
||||
|
||||
Продлить ответственность конвейера за прод **после** `deploy → done`: в течение
|
||||
заданного окна наблюдать ключевые сигналы здоровья прода и при доказанной деградации
|
||||
выполнить реакцию (откат на предыдущий образ или громкий алерт с запросом ручного
|
||||
отката). Закрыть класс «зелёный деплой, красный прод».
|
||||
|
||||
Механизм частичного отката уже есть: `do_rollback()` и режим `--rollback` в
|
||||
`scripts/orchestrator-deploy-hook.sh` умеют вернуть предыдущий образ из
|
||||
`PREV_IMAGE_FILE` (`.deploy-prev-image-prod`), который сохраняется при каждом деплое.
|
||||
Задача — построить **наблюдение поверх** этого и привязать решение к измеримым порогам.
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
- **Owner (Слава)** — принимает риск авто-отката прода; получает алерты.
|
||||
- **Стрим** — инициатор; потребитель сигнала деградации для петли уроков (ORCH-8).
|
||||
- **Другие проекты (enduro-trails)** — косвенно: устойчивость общего инстанса.
|
||||
|
||||
## 4. Бизнес-требования
|
||||
|
||||
| # | Требование | Приоритет |
|
||||
|---|------------|-----------|
|
||||
| BR-1 | После `deploy → done` прод наблюдается в течение конфигурируемого окна (дефолт ~15 мин), а не забывается. | Must |
|
||||
| BR-2 | Деградация определяется по **детерминированным измеримым сигналам**: периодический `/health` (HTTP 200 + `{"status":"ok"}`) и доля HTTP 5xx на ключевых эндпоинтах (`/status`, `/queue`). | Must |
|
||||
| BR-3 | Деградация фиксируется только по **порогам** (N последовательных провалов / окно), а не по разовому сетевому глюку — чтобы не было ложных откатов. | Must |
|
||||
| BR-4 | При подтверждённой деградации система выполняет реакцию: **авто-rollback** на `.deploy-prev-image-prod` (через существующий хук `--rollback`) **либо** громкий алерт с запросом ручного отката — в зависимости от политики репозитория. | Must |
|
||||
| BR-5 | **Self-hosting safety:** для самого `orchestrator` авто-откат прода = рестарт инструмента, обслуживающего все проекты. По умолчанию для self-hosting реакция — **алерт + ручной approve отката** (по образцу deploy Phase A/B), НЕ автоматический откат. Для не-self репозиториев допустим авто-откат. | Must |
|
||||
| BR-6 | Любой исход (наблюдение начато, деградация, откат, откат-провал, окно завершилось чисто) уведомляется в Telegram и комментарием в Plane; результат наблюдения фиксируется артефактом. | Must |
|
||||
| BR-7 | Мониторинг — **restart-safe**: рестарт оркестратора (в т.ч. сам деплой) не теряет и не задваивает наблюдение. Идемпотентность по образцу reconciler / deploy-finalizer. | Must |
|
||||
| BR-8 | Глобальный kill-switch (env-флаг) и список репозиториев, на которые распространяется фича (по образцу `merge_gate_enabled` / `image_freshness_enabled` / `self_deploy_repos`). Выключенный флаг = прежнее поведение (наблюдения нет). | Must |
|
||||
| BR-9 | Наблюдаемость: текущее состояние пост-деплой наблюдения отражается в `GET /queue` (по образцу блока `reconcile`). | Should |
|
||||
| BR-10 | Сигнал деградации пригоден для будущей петли уроков (ORCH-8): фиксируется в артефакте/логе в машиночитаемом виде. | Should |
|
||||
| BR-11 | Доменный smoke результата фичи (проверка, что конкретная фича реально работает) — желателен, но выносится в follow-up; MVP ограничивается health + 5xx. | Could |
|
||||
|
||||
## 5. Вне рамок (Out of scope)
|
||||
- Полноценная система метрик/APM (Prometheus, дашборды) — фича опирается на уже
|
||||
существующие HTTP-эндпоинты, не вводит сбор метрик.
|
||||
- Универсальный доменный smoke для произвольной фичи (BR-11 — follow-up).
|
||||
- Полностью автоматический откат прод-орка без участия человека (противоречит
|
||||
self-hosting safety; отдельная задача при наборе доверия, аналогично ORCH-54 для deploy).
|
||||
- Изменение момента вердикта `deploy_status` / контракта `check_deploy_status`
|
||||
(наблюдение происходит ПОСЛЕ `done`, не заменяет deploy-gate).
|
||||
|
||||
## 6. Связи
|
||||
- **ET-8** — прецедент «deploy SUCCESS, прод не работает». Обоснование задачи.
|
||||
- **ORCH-36** (`docs/architecture/adr/adr-0007-executable-self-deploy.md`) — Phase A/B/C
|
||||
исполняемого самодеплоя; пост-деплой наблюдение продлевает ответственность ЗА `done`,
|
||||
переиспользует sentinel-паттерн и detached-host-процесс для self-rollback.
|
||||
- **ORCH-53** (`src/reconciler.py`) — каноничный паттерн фонового daemon-потока
|
||||
(watchdog), запускаемого в `main.lifespan`; образец для пост-деплой наблюдателя.
|
||||
- **ORCH-58** — `.deploy-prev-image` и хук-механика отката, на которые опирается реакция.
|
||||
- **ORCH-8** — деградация прода = сигнал для петли уроков (BR-10).
|
||||
- **ORCH-12** — фича может оформиться как пост-deploy стадия ИЛИ как watchdog (решение
|
||||
архитектора, см. §7).
|
||||
|
||||
## 7. Открытые архитектурные вопросы (для архитектора, НЕ решаются в анализе)
|
||||
1. **Где живёт наблюдение:** отдельная пост-deploy стадия конвейера vs фоновый
|
||||
watchdog-daemon (по образцу `reconciler`) vs reserved-agent job (по образцу
|
||||
`deploy-finalizer`). Анализ задаёт требования (BR-1, BR-7), выбор механизма — за архитектором.
|
||||
2. **Механизм self-rollback для self-hosting:** откат прод-орка требует detached
|
||||
host-процесса (контейнер не может надёжно откатить себя, умирая) — переиспользовать
|
||||
ли `self_deploy.initiate_deploy` / хук `--rollback`.
|
||||
3. Точные пороги и веса сигналов (BR-3) — анализ предлагает дефолты (см. AC), архитектор
|
||||
фиксирует реализацию.
|
||||
165
docs/work-items/ORCH-021/02-trz.md
Normal file
165
docs/work-items/ORCH-021/02-trz.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# ТЗ — ORCH-021: Post-deploy мониторинг прода + авто-rollback
|
||||
|
||||
Work Item: ORCH-021
|
||||
Стадия: analysis → (architecture)
|
||||
|
||||
> Документ описывает ТРЕБОВАНИЯ к изменениям и НАЗЫВАЕТ задействованные модули.
|
||||
> Выбор механизма (стадия vs watchdog vs reserved-agent) и точная реализация —
|
||||
> зона архитектора (см. BRD §7). Здесь фиксируется, ЧТО должно измениться и КАКИЕ
|
||||
> контракты НЕЛЬЗЯ ломать.
|
||||
|
||||
## 1. Контекст в коде (как есть сейчас)
|
||||
|
||||
- Конвейер заканчивается в `src/stages.py`: `deploy → done`, gate `check_deploy_status`.
|
||||
Терминальный переход `deploy → done` исполняется в `src/stage_engine.py::advance_stage`
|
||||
(блок «Terminal sync», `set_issue_done`, release merge-lease). После этого ничего
|
||||
не наблюдает за продом.
|
||||
- `scripts/orchestrator-deploy-hook.sh` уже умеет:
|
||||
- `health_check(max_attempts, sleep, label)` — опрос `http://localhost:$TARGET_PORT/health`
|
||||
с проверкой `"status":"ok"`;
|
||||
- `do_rollback()` — retag `PREV_IMAGE_FILE` → `TARGET_IMAGE` + рестарт + пост-rollback
|
||||
health-check; коды возврата 0 (ок) / 1 (нет prev-образа) / 2 (rollback тоже упал);
|
||||
- режим `--rollback` (ручной откат);
|
||||
- при обычном деплое сохраняет `PREV_IMG` в `PREV_IMAGE_FILE`
|
||||
(`.deploy-prev-image-prod` для прода, см. `settings.deploy_prod_prev_image_file`).
|
||||
- Self-deploy прода идёт через detached host-процесс: `src/self_deploy.py`
|
||||
(`build_deploy_command`, `initiate_deploy`, sentinel-маркеры под
|
||||
`.deploy-state-<repo>/<wi>/`, `read_result`, `map_exit_code_to_status`).
|
||||
- Фоновый daemon-паттерн: `src/reconciler.py` (`threading.Thread(daemon=True)` +
|
||||
`threading.Event`, старт/стоп в `src/main.py::lifespan` после `worker.start()` /
|
||||
перед `worker.stop()`, `status()` в `GET /queue`).
|
||||
- Reserved-agent (детерминированный no-LLM job) паттерн: `deploy-finalizer` —
|
||||
перехват в `src/agents/launcher.py::launch_job` ДО `_spawn`, исполнение
|
||||
`stage_engine.run_deploy_finalizer`, отложенная постановка через
|
||||
`enqueue_job(..., available_at_delay_s=...)`.
|
||||
- Условность self-hosting: `src/qg/checks.py::is_self_hosting_repo`,
|
||||
`src/self_deploy.py::self_deploy_applies` (флаг + CSV-репо; пусто → только `orchestrator`).
|
||||
- Наблюдаемые эндпоинты прода (`src/main.py`): `GET /health`, `GET /status`, `GET /queue`.
|
||||
- API БД: `src/db.py::enqueue_job` (с `available_at_delay_s`), `get_db`,
|
||||
`update_task_stage`, `get_active_tasks_for_reconcile`.
|
||||
|
||||
## 2. Требуемые изменения
|
||||
|
||||
### 2.1. Новый leaf-модуль чистой логики наблюдения — `src/post_deploy.py` (новый)
|
||||
Контракт **never-raise** (по образцу `self_deploy.py` / `staging_verdict.py`).
|
||||
Чистые, юнит-тестируемые функции:
|
||||
- **Опрос сигналов:** функция, опрашивающая `/health` и ключевые эндпоинты
|
||||
(`/status`, `/queue`) прод-инстанса (base-url из config), возвращающая структуру
|
||||
с результатами (код ответа, ok-флаг, доля 5xx). Сеть/таймаут → консервативный
|
||||
результат, не исключение.
|
||||
- **Классификация деградации** (чистая, без сети): на вход — серия результатов
|
||||
опросов; на выход — вердикт `HEALTHY | DEGRADED` по порогам (BR-3):
|
||||
`≥ post_deploy_fail_threshold` последовательных провалов health ИЛИ доля 5xx
|
||||
выше `post_deploy_5xx_threshold` на окне. Эта функция — основной предмет
|
||||
юнит-тестов (детерминированная, как `compute_staging_verdict` в ORCH-061).
|
||||
- **Решение о реакции** (чистая): по `(repo, вердикт, политика)` → одно из
|
||||
`NONE | ROLLBACK | ALERT_ONLY`, с учётом self-hosting (BR-5).
|
||||
- **Запись артефакта** результата наблюдения (см. §2.5), best-effort.
|
||||
- Условность: хелпер `post_deploy_applies(repo)` (флаг + CSV-репо, пусто →
|
||||
только self-hosting), по образцу `self_deploy_applies` / `_merge_gate_applies`.
|
||||
|
||||
### 2.2. Оркестрация наблюдения (механизм — выбор архитектора)
|
||||
Требования к механизму (независимо от выбора стадия/watchdog/reserved-agent):
|
||||
- запускается ПОСЛЕ перехода `deploy → done` для применимого репозитория (BR-1);
|
||||
- наблюдает окно `post_deploy_window_s` с интервалом `post_deploy_interval_s`;
|
||||
- **restart-safe и идемпотентен** (BR-7): состояние наблюдения — в sentinel-файлах
|
||||
(по образцу `.deploy-state-<repo>/<wi>/`, напр. маркеры `monitor-started` /
|
||||
`monitor-done`) ИЛИ через отложенные `enqueue_job(available_at_delay_s=...)`;
|
||||
повторный старт не задваивает наблюдение и не теряет его при рестарте;
|
||||
- по итогу вызывает «Решение о реакции» из `src/post_deploy.py` и исполняет реакцию (§2.3).
|
||||
|
||||
Кандидатные точки интеграции (на выбор архитектора, см. BRD §7):
|
||||
- хук в `stage_engine.advance_stage` в блоке `next_stage == "done"` — арм наблюдения;
|
||||
- reserved-agent `post-deploy-monitor` (расширение `launcher.launch_job` ДО `_spawn`,
|
||||
как `deploy-finalizer`), с само-перепостановкой через `available_at_delay_s`;
|
||||
- отдельный daemon-поток `PostDeployWatcher` (как `Reconciler`), старт/стоп в `main.lifespan`.
|
||||
|
||||
### 2.3. Реакция на деградацию
|
||||
- **Не-self репозитории / политика auto:** вызвать существующий хук в режиме отката
|
||||
(`scripts/orchestrator-deploy-hook.sh --rollback` с прод-параметрами окружения,
|
||||
как в `self_deploy.build_deploy_command`, но action=`--rollback`). Маппинг
|
||||
exit-code хука (0/1/2) в исход переиспользует логику `self_deploy.map_exit_code_to_status`
|
||||
по смыслу (0 → откат успешен; 1/2 → откат не выполнен/провалился → громкий алерт).
|
||||
- **Self-hosting (`orchestrator`) по умолчанию (BR-5):** НЕ откатывать автоматически.
|
||||
Сформировать громкий алерт (Telegram + Plane-коммент) и запросить ручной approve
|
||||
отката (по образцу deploy Phase A — статус Plane / Telegram CTA). Откат самого
|
||||
прод-орка, если выполняется, — только через detached host-процесс (нельзя надёжно
|
||||
откатить контейнер, который при этом умирает; переиспользовать механику
|
||||
`self_deploy.initiate_deploy`).
|
||||
- Команда отката для self НЕ должна ронять прод-контейнер в рамках обычного тика
|
||||
наблюдения (CLAUDE.md: не ронять/не рестартить прод-контейнер вне явного действия).
|
||||
|
||||
### 2.4. Конфигурация — `src/config.py` (расширение `Settings`)
|
||||
Добавить (env-префикс `ORCH_`, дефолты безопасные):
|
||||
- `post_deploy_monitor_enabled: bool = True` — глобальный kill-switch (BR-8).
|
||||
- `post_deploy_repos: str = ""` — CSV применимых репо; пусто → только self-hosting
|
||||
(по образцу `self_deploy_repos` / `merge_gate_repos` / `image_freshness_repos`).
|
||||
- `post_deploy_window_s: int = 900` — длина окна наблюдения (дефолт ~15 мин, BR-1).
|
||||
- `post_deploy_interval_s: int = 30` — интервал между опросами.
|
||||
- `post_deploy_fail_threshold: int = 3` — N последовательных провалов health → DEGRADED.
|
||||
- `post_deploy_5xx_threshold: float = 0.5` — порог доли 5xx на окне → DEGRADED.
|
||||
- `post_deploy_auto_rollback: bool = False` — глобально разрешён ли авто-откат;
|
||||
при `True` действует для не-self репо; для self всегда требует approve (BR-5).
|
||||
- `post_deploy_base_url: str = "http://localhost:8500"` — base-url наблюдаемого прода.
|
||||
- `post_deploy_target` параметры отката — переиспользовать существующие
|
||||
`deploy_prod_*` (service/port/image/prev_image_file), новых дублей не вводить.
|
||||
|
||||
### 2.5. Артефакт задачи — `16-post-deploy-log.md` (новый)
|
||||
В `docs/work-items/<plane-id>/`. YAML-frontmatter (машиночитаемо, канон гейтов;
|
||||
для будущей петли уроков BR-10):
|
||||
```
|
||||
---
|
||||
post_deploy_status: HEALTHY | DEGRADED
|
||||
action_taken: NONE | ROLLBACK_OK | ROLLBACK_FAILED | ALERT_ONLY
|
||||
work_item: <plane-id>
|
||||
window_s: <int>
|
||||
checks_total: <int>
|
||||
checks_failed: <int>
|
||||
---
|
||||
```
|
||||
Тело — человекочитаемая сводка опросов. Записывается best-effort (по образцу
|
||||
`self_deploy.write_deploy_log`); отсутствие файла не должно ничего ронять.
|
||||
> Артефакт `16-post-deploy-log.md` добавить в перечень артефактов в `CLAUDE.md`
|
||||
> и таблицу/описание в `docs/architecture/README.md` (golden-source, в том же PR).
|
||||
|
||||
### 2.6. Наблюдаемость — `GET /queue` (`src/main.py`) (BR-9)
|
||||
Добавить блок `post_deploy` со снимком состояния (enabled, window, активные
|
||||
наблюдения, последний исход) — по образцу блока `reconcile` (метод `status()`).
|
||||
|
||||
### 2.7. Изменения схемы БД
|
||||
**Не требуются.** Состояние наблюдения — sentinel-файлы (restart-safe, без миграции,
|
||||
по образцу ORCH-36) и/или отложенные jobs. Если архитектор выберет колонку в `tasks`
|
||||
для отметки наблюдения — потребуется миграция; предпочтительно избежать (как ORCH-36/53/58).
|
||||
|
||||
### 2.8. Новые QG checks
|
||||
**Не требуются.** Наблюдение происходит ПОСЛЕ `done` и не является gate'ом стадии;
|
||||
реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются (если архитектор НЕ выберет
|
||||
вариант «отдельная пост-deploy стадия» — тогда потребуется новая стадия+gate, что
|
||||
надо явно отразить в ADR; по умолчанию предпочтителен вариант без изменения реестров).
|
||||
|
||||
## 3. Инварианты (НЕ ломать)
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, контракт `check_deploy_status` /
|
||||
`_parse_deploy_status`, момент вердикта `deploy_status`, БАГ-8 откат, terminal-sync
|
||||
`deploy → done`, merge-gate, exit-code-контракт хука (0/1/2) — без изменений.
|
||||
- Контракт хука: дефолты STAGING-безопасны; прод-параметры приходят только через env.
|
||||
- Условность как ORCH-35/36/43/58: реально для `orchestrator`/listed-repos, прочие — no-op.
|
||||
- Never-raise: ошибка в наблюдении не роняет worker / lifespan / конвейер других проектов.
|
||||
- Self-hosting: тик наблюдения НИКОГДА не рестартит прод-контейнер сам по себе (BR-5).
|
||||
|
||||
## 4. Задействованные модули (сводка)
|
||||
| Модуль | Изменение |
|
||||
|--------|-----------|
|
||||
| `src/post_deploy.py` | **новый** — чистая логика опроса/классификации/решения/артефакта, never-raise |
|
||||
| `src/config.py` | +параметры `post_deploy_*` (kill-switch, окно, пороги, политика) |
|
||||
| `src/stage_engine.py` и/или `src/agents/launcher.py` и/или `src/main.py` | арм/исполнение наблюдения (точка — за архитектором) |
|
||||
| `scripts/orchestrator-deploy-hook.sh` | переиспользуется (`--rollback`); правки — только если откат self требует отдельной ветки (за архитектором) |
|
||||
| `src/main.py` | блок `post_deploy` в `GET /queue` (BR-9); возможный старт daemon в `lifespan` |
|
||||
| `docs/work-items/<id>/16-post-deploy-log.md` | **новый** артефакт |
|
||||
| `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` | обновить (golden-source, в том же PR) |
|
||||
| ADR | `docs/work-items/ORCH-021/06-adr/ADR-001-*.md` (+ возможный сквозной `adr/adr-00NN`) |
|
||||
|
||||
## 5. Артефакты по pipeline, которые должны появиться/обновиться
|
||||
- `16-post-deploy-log.md` (новый, машиночитаемый frontmatter).
|
||||
- Обновлённые `CLAUDE.md` (перечень артефактов), `docs/architecture/README.md`
|
||||
(описание пост-деплой наблюдения), `CHANGELOG.md`.
|
||||
- ADR work-item (`06-adr/`) с зафиксированным выбором механизма и порогов.
|
||||
106
docs/work-items/ORCH-021/03-acceptance-criteria.md
Normal file
106
docs/work-items/ORCH-021/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Критерии приёмки — ORCH-021
|
||||
|
||||
Work Item: ORCH-021
|
||||
Формат: каждый критерий имеет чёткое условие PASS/FAIL и проверяется тестом
|
||||
из `04-test-plan.yaml`.
|
||||
|
||||
## Наблюдение и сигналы
|
||||
|
||||
### AC-1 — наблюдение армится после deploy→done
|
||||
- **PASS:** для применимого репозитория после терминального перехода `deploy → done`
|
||||
пост-деплой наблюдение инициируется (создаётся sentinel/отложенный job/запись в watcher).
|
||||
- **FAIL:** переход `deploy → done` не приводит к старту наблюдения.
|
||||
|
||||
### AC-2 — наблюдение НЕ армится для неприменимых репо
|
||||
- **PASS:** для репозитория вне области (не self-hosting и не в `post_deploy_repos`)
|
||||
`post_deploy_applies(repo)` → False; наблюдение не стартует; конвейер не меняется.
|
||||
- **FAIL:** наблюдение стартует для неприменимого репо.
|
||||
|
||||
### AC-3 — классификация HEALTHY
|
||||
- **PASS:** серия опросов без провалов (или провалов меньше `post_deploy_fail_threshold`
|
||||
и доля 5xx ниже `post_deploy_5xx_threshold`) → вердикт `HEALTHY`.
|
||||
- **FAIL:** при здоровых сигналах возвращается `DEGRADED`.
|
||||
|
||||
### AC-4 — классификация DEGRADED по порогу провалов health
|
||||
- **PASS:** `≥ post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов health → `DEGRADED`.
|
||||
- **FAIL:** порог достигнут, но вердикт не `DEGRADED`.
|
||||
|
||||
### AC-5 — классификация DEGRADED по доле 5xx
|
||||
- **PASS:** доля 5xx на окне выше `post_deploy_5xx_threshold` → `DEGRADED`,
|
||||
даже если `/health` отвечает 200.
|
||||
- **FAIL:** превышение порога 5xx не даёт `DEGRADED`.
|
||||
|
||||
### AC-6 — устойчивость к разовому глюку (нет ложного срабатывания)
|
||||
- **PASS:** одиночный провал (1 < `post_deploy_fail_threshold`) с последующим
|
||||
восстановлением → итог `HEALTHY`, реакции нет.
|
||||
- **FAIL:** одиночный разовый провал приводит к `DEGRADED`/откату.
|
||||
|
||||
## Реакция
|
||||
|
||||
### AC-7 — авто-rollback для не-self репо при политике auto
|
||||
- **PASS:** при `post_deploy_auto_rollback=True` и НЕ-self репо вердикт `DEGRADED`
|
||||
приводит к вызову отката (хук `--rollback` с прод-параметрами); `action_taken`
|
||||
фиксируется как `ROLLBACK_OK`/`ROLLBACK_FAILED` по exit-code.
|
||||
- **FAIL:** откат не вызывается, либо вызывается с staging-дефолтами, либо роняет прод напрямую.
|
||||
|
||||
### AC-8 — self-hosting НЕ откатывается автоматически (safety)
|
||||
- **PASS:** для `orchestrator` вердикт `DEGRADED` НЕ приводит к автоматическому
|
||||
откату/рестарту прод-контейнера в тике наблюдения; вместо этого формируется
|
||||
громкий алерт + запрос ручного approve (`action_taken: ALERT_ONLY`).
|
||||
- **FAIL:** тик наблюдения автоматически откатывает/рестартит прод-орк.
|
||||
|
||||
### AC-9 — откат-провал эскалируется
|
||||
- **PASS:** если откат вызван и вернул код 1/2 (нет prev-образа / откат тоже упал) →
|
||||
`action_taken: ROLLBACK_FAILED` + громкий Telegram-алерт о необходимости ручного вмешательства.
|
||||
- **FAIL:** провал отката проглатывается тихо.
|
||||
|
||||
## Конфигурация и совместимость
|
||||
|
||||
### AC-10 — kill-switch выключает фичу
|
||||
- **PASS:** `post_deploy_monitor_enabled=False` → наблюдение не армится ни для кого;
|
||||
поведение конвейера 1:1 как до ORCH-021.
|
||||
- **FAIL:** при выключенном флаге наблюдение всё равно работает.
|
||||
|
||||
### AC-11 — пороги/окно конфигурируемы через env
|
||||
- **PASS:** `post_deploy_window_s`, `post_deploy_interval_s`, `post_deploy_fail_threshold`,
|
||||
`post_deploy_5xx_threshold` читаются из `Settings` (env `ORCH_*`) и влияют на поведение.
|
||||
- **FAIL:** значения захардкожены.
|
||||
|
||||
### AC-12 — реестры и схема БД не изменены
|
||||
- **PASS:** `STAGE_TRANSITIONS`, `QG_CHECKS`, контракт `check_deploy_status` и схема
|
||||
таблиц БД не изменены (если архитектор не вводит явно новую стадию — тогда это
|
||||
отражено в ADR и тестах). Существующие тесты deploy/staging/merge-gate зелёные.
|
||||
- **FAIL:** молча сломан какой-либо существующий контракт/тест.
|
||||
|
||||
## Наблюдаемость, артефакт, идемпотентность
|
||||
|
||||
### AC-13 — артефакт 16-post-deploy-log.md с машиночитаемым frontmatter
|
||||
- **PASS:** по итогу наблюдения пишется `16-post-deploy-log.md` с валидным YAML-frontmatter
|
||||
(`post_deploy_status`, `action_taken`); запись best-effort (её отсутствие ничего не роняет).
|
||||
- **FAIL:** артефакт не пишется или frontmatter невалиден/непарсится.
|
||||
|
||||
### AC-14 — наблюдаемость в /queue
|
||||
- **PASS:** `GET /queue` содержит блок `post_deploy` со снимком состояния (enabled,
|
||||
window, активные/последний исход).
|
||||
- **FAIL:** состояние наблюдения нигде не видно.
|
||||
|
||||
### AC-15 — идемпотентность / restart-safe
|
||||
- **PASS:** повторный арм для той же задачи (двойной webhook / рестарт оркестратора)
|
||||
не создаёт второе параллельное наблюдение и не теряет уже идущее.
|
||||
- **FAIL:** дублируется наблюдение или теряется при рестарте.
|
||||
|
||||
### AC-16 — never-raise
|
||||
- **PASS:** любая ошибка опроса/сети/файлов/классификации логируется и НЕ роняет
|
||||
worker / lifespan / конвейер других проектов.
|
||||
- **FAIL:** исключение из наблюдения всплывает и ломает обслуживание других проектов.
|
||||
|
||||
### AC-17 — уведомления
|
||||
- **PASS:** ключевые события (наблюдение начато, DEGRADED, откат/алерт, чистое
|
||||
завершение окна) уведомляются в Telegram и/или Plane-комментарием.
|
||||
- **FAIL:** деградация/откат происходят молча.
|
||||
|
||||
### AC-18 — документация обновлена (golden-source)
|
||||
- **PASS:** в том же PR обновлены `CLAUDE.md` (артефакт `16-post-deploy-log.md`),
|
||||
`docs/architecture/README.md` (описание пост-деплой наблюдения), `CHANGELOG.md`,
|
||||
и заведён ADR work-item.
|
||||
- **FAIL:** функционал есть, документация не обновлена (reviewer → REQUEST_CHANGES).
|
||||
163
docs/work-items/ORCH-021/04-test-plan.yaml
Normal file
163
docs/work-items/ORCH-021/04-test-plan.yaml
Normal file
@@ -0,0 +1,163 @@
|
||||
work_item: ORCH-021
|
||||
description: >
|
||||
Тест-план пост-деплой мониторинга прода + авто-rollback. Упор на детерминированную
|
||||
чистую логику классификации/решения (юнит, без сети/LLM) и на интеграцию
|
||||
армирования наблюдения после deploy->done. Сетевые опросы и хук-вызовы мокируются.
|
||||
Имена модулей/функций — целевые (src/post_deploy.py); архитектор уточняет точную
|
||||
сигнатуру, тесты адаптируются под ADR.
|
||||
|
||||
tests:
|
||||
# --- Классификация деградации (чистая логика, ядро) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "HEALTHY: серия опросов без провалов (< порога) -> вердикт HEALTHY"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-3]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "DEGRADED: N последовательных провалов health (== fail_threshold) -> DEGRADED"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-4]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "DEGRADED по 5xx: доля 5xx выше порога при health=200 -> DEGRADED"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-5]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "Нет ложного срабатывания: одиночный провал (1 < threshold) + восстановление -> HEALTHY"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-6]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "Пороги читаются из Settings (env ORCH_*), изменение порога меняет вердикт на тех же данных"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-11]
|
||||
expected: PASS
|
||||
|
||||
# --- Решение о реакции (чистая логика + self-hosting safety) ---
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "Решение: не-self репо + auto_rollback=True + DEGRADED -> ROLLBACK"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-7]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "Решение self-hosting: orchestrator + DEGRADED -> ALERT_ONLY (НИКОГДА не авто-rollback)"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-8]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Решение: HEALTHY -> NONE (реакции нет) для любого репо"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-3]
|
||||
expected: PASS
|
||||
|
||||
# --- Условность / kill-switch ---
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "post_deploy_applies: пусто в repos -> True только для orchestrator, False для enduro-trails"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-2]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "kill-switch: post_deploy_monitor_enabled=False -> applies()=False для всех; наблюдение не армится"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-10]
|
||||
expected: PASS
|
||||
|
||||
# --- Маппинг exit-code отката -> исход ---
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Откат exit 0 -> action_taken=ROLLBACK_OK"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-7]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Откат exit 1/2 (нет prev-образа / откат упал) -> ROLLBACK_FAILED + эскалация-алерт"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-9]
|
||||
expected: PASS
|
||||
|
||||
# --- Артефакт ---
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "16-post-deploy-log.md пишется с валидным YAML-frontmatter (post_deploy_status/action_taken), парсится yaml.safe_load"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-13]
|
||||
expected: PASS
|
||||
|
||||
# --- never-raise ---
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "Опрос при сетевой ошибке/таймауте -> консервативный результат (провал-как-down), исключение НЕ всплывает"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-16]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "Ошибка записи артефакта (нет каталога/IO) -> логируется, функция возвращает False, не raise"
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-16, AC-13]
|
||||
expected: PASS
|
||||
|
||||
# --- Интеграция: армирование после deploy->done ---
|
||||
- id: TC-16
|
||||
type: integration
|
||||
description: "advance_stage deploy->done для orchestrator армит наблюдение (sentinel/job создан); для enduro-trails — нет"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-1, AC-2]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "Идемпотентность: повторный арм той же задачи (двойной webhook) не создаёт второе наблюдение"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-15]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "Полный цикл DEGRADED -> для не-self вызывается откат (хук замокан), пишется лог, шлётся уведомление"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-7, AC-13, AC-17]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: integration
|
||||
description: "Self-hosting DEGRADED: тик НЕ вызывает рестарт/откат прод-контейнера, формирует алерт+approve-запрос"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-8, AC-17]
|
||||
expected: PASS
|
||||
|
||||
# --- Наблюдаемость и обратная совместимость ---
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "GET /queue содержит блок post_deploy со снимком состояния"
|
||||
module: tests/test_post_deploy_integration.py
|
||||
covers: [AC-14]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: integration
|
||||
description: "Регресс: существующие тесты deploy/staging/merge-gate/reconciler зелёные; STAGE_TRANSITIONS и QG_CHECKS не изменены"
|
||||
module: tests/test_stages.py
|
||||
covers: [AC-12]
|
||||
expected: PASS
|
||||
212
docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md
Normal file
212
docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# ADR-001 (ORCH-021): Post-deploy мониторинг прода + реакция на деградацию
|
||||
|
||||
## Статус
|
||||
Proposed (design) — реализация в ветке `feature/ORCH-021-post-deploy-rollback`.
|
||||
Сквозной индексный ADR: `docs/architecture/adr/adr-0010-post-deploy-monitor.md`.
|
||||
Помечено `arch:major-change` (новая под-компонента + новый reserved-agent job-kind).
|
||||
|
||||
## Контекст
|
||||
Конвейер заканчивается на `deploy → done` (`check_deploy_status` видит
|
||||
`deploy_status: SUCCESS` → terminal-sync, Plane → Done, release merge-lease). После
|
||||
этого оркестратор **забывает про прод**. «Успех» сегодня = прохождение health-check
|
||||
в момент рестарта (10×6с в `scripts/orchestrator-deploy-hook.sh`) — узкое окно ~60с.
|
||||
|
||||
Класс инцидентов «зелёный деплой, красный прод» (прецедент **ET-8**): деградация
|
||||
проявляется через минуты под боевым трафиком (прогрев кэшей, фоновые миграции,
|
||||
утечки, рост 5xx), health отвечает `200 ok`, но фича сломана. Для self-hosting это
|
||||
критично: сломанный прод-орк (8500) обслуживает ВСЕ проекты из общего инстанса.
|
||||
|
||||
BRD/ТЗ задают требования (BR-1…BR-11, AC-1…AC-18) и оставляют архитектору **три
|
||||
открытых вопроса** (BRD §7): (1) где живёт наблюдение — стадия / watchdog-daemon /
|
||||
reserved-agent job; (2) механизм self-rollback; (3) пороги/веса сигналов.
|
||||
|
||||
Существующие переиспользуемые механики:
|
||||
- **deploy-finalizer** (ORCH-36, `stage_engine.run_deploy_finalizer` + перехват в
|
||||
`launcher.launch_job` ДО `_spawn`) — детерминированный no-LLM reserved-agent job,
|
||||
само-перепостановка через `enqueue_job(available_at_delay_s=...)`, defer-budget,
|
||||
restart-safe (jobs-очередь + sentinel-файлы `.deploy-state-<repo>/<wi>/`).
|
||||
- **self_deploy.py** — sentinel-state хелперы (`write_marker`/`has_marker`/
|
||||
`read_result`/`clear_state`), detached host-процесс (`build_deploy_command`/
|
||||
`initiate_deploy`: ssh + setsid), `map_exit_code_to_status`, `self_deploy_applies`.
|
||||
- **reconciler.py** — daemon-поток + `status()` в `GET /queue`.
|
||||
- **хук `--rollback`** (`do_rollback`): retag `PREV_IMAGE_FILE` → `TARGET_IMAGE` +
|
||||
рестарт + health, коды 0 / 1 (нет prev-образа) / 2 (rollback тоже упал).
|
||||
- **Условность** ORCH-35/36/43/58: `is_self_hosting_repo`, флаг + CSV-репо.
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Механизм наблюдения — reserved-agent job `post-deploy-monitor` (Вариант B)
|
||||
Наблюдение реализуется как **детерминированный no-LLM reserved-agent job**, точная
|
||||
калька **deploy-finalizer**. Один «тик» наблюдения = один job: он делает ОДИН опрос
|
||||
сигналов, обновляет персистентные счётчики в sentinel-файлах, классифицирует и либо
|
||||
**перепостанавливает себя** с задержкой `post_deploy_interval_s` (окно не истекло и
|
||||
ещё не DEGRADED), либо завершает наблюдение (DEGRADED → реакция; либо окно истекло →
|
||||
HEALTHY). Это «watchdog поверх очереди»: между тиками job не выполняется (он
|
||||
запланирован в будущем через `available_at_delay_s`), worker свободен для других
|
||||
проектов — ровно как defer у finalizer.
|
||||
|
||||
**Почему НЕ daemon-watchdog (Вариант A, как reconciler):** daemon тикает глобально, а
|
||||
не per-task; серию опросов (последовательные провалы health, доля 5xx на окне) пришлось
|
||||
бы держать в памяти → теряется/двоится при рестарте (а сам деплой орка = рестарт). Чтобы
|
||||
сделать daemon restart-safe, всё равно нужны персистентные per-task счётчики в sentinel —
|
||||
тогда reserved-agent проще и уже имеет проверенную restart-safe машинерию (jobs-очередь
|
||||
+ `requeue_running_jobs` + sentinels). Per-task жизненный цикл естественно ложится на
|
||||
job-цепочку, а не на глобальный sweep.
|
||||
|
||||
**Почему НЕ отдельная пост-deploy стадия (Вариант C):** меняет `STAGE_TRANSITIONS` +
|
||||
реестр `QG_CHECKS` (нарушает AC-12, ТЗ §2.8 — явно непредпочтительно); ломает семантику
|
||||
`deploy → done` как терминального перехода (Plane уже Done). Наблюдение происходит
|
||||
**ПОСЛЕ** `done` — «продление ответственности ЗА done», а не новая стадия конвейера.
|
||||
|
||||
### 2. Арм наблюдения — хук в terminal-блоке `advance_stage`
|
||||
В `stage_engine.advance_stage`, в существующем блоке `next_stage == "done"` (после
|
||||
`set_issue_done` и `release_merge_lease`), добавляется арм:
|
||||
```
|
||||
if next_stage == "done" and post_deploy.post_deploy_applies(repo):
|
||||
post_deploy.arm_monitor(repo, work_item_id, branch, task_id)
|
||||
```
|
||||
`arm_monitor` (never-raise): если sentinel `armed` отсутствует → создаёт state-dir,
|
||||
пишет `armed` (идемпотентность, по образцу `INITIATED`), инициализирует `series`-файл,
|
||||
ставит первый `post-deploy-monitor` job через `enqueue_job(available_at_delay_s=
|
||||
post_deploy_interval_s)`. Если `armed` уже есть → no-op (двойной webhook / reconciler
|
||||
F-1 / finalizer Phase C могут довести `done` повторно — AC-15). Выключенный
|
||||
kill-switch / неприменимый репо → `post_deploy_applies` False → арма нет (AC-2/AC-10).
|
||||
|
||||
### 3. Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise)
|
||||
По образцу `self_deploy.py` / `staging_verdict.py`. Импортирует только config (+lazy
|
||||
`qg.checks.is_self_hosting_repo`), НЕ импортирует `stage_engine`/`launcher`. Функции:
|
||||
- **`post_deploy_applies(repo) -> bool`** — флаг `post_deploy_monitor_enabled` +
|
||||
CSV `post_deploy_repos` (пусто → только self-hosting). Калька `self_deploy_applies`.
|
||||
- **`probe_signals(base_url) -> ProbeResult`** — один опрос: `GET /health` (HTTP 200 +
|
||||
`{"status":"ok"}`) и ключевые эндпоинты `/status`, `/queue` (учёт доли 5xx).
|
||||
Сеть/таймаут → консервативный «провал»-результат, не исключение.
|
||||
- **`classify(series, fail_threshold, 5xx_threshold) -> "HEALTHY"|"DEGRADED"`** —
|
||||
чистая, без сети, **главный предмет юнит-тестов** (детерминированная, как
|
||||
`compute_staging_verdict`): `DEGRADED` если `≥ fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ
|
||||
провалов health (AC-4) ИЛИ доля 5xx на окне `> 5xx_threshold` (AC-5). Иначе
|
||||
`HEALTHY` (одиночный провал < порога с восстановлением → HEALTHY, AC-3/AC-6).
|
||||
- **`decide_action(repo, verdict) -> "NONE"|"ROLLBACK"|"ALERT_ONLY"`** — чистая:
|
||||
`HEALTHY → NONE`; `DEGRADED` + self-hosting → `ALERT_ONLY` (BR-5/AC-8, ВСЕГДА);
|
||||
`DEGRADED` + не-self + `post_deploy_auto_rollback=True` → `ROLLBACK`; иначе →
|
||||
`ALERT_ONLY`.
|
||||
- **Sentinel-state хелперы** (state-dir `.post-deploy-state-<repo>/<wi>/`, по образцу
|
||||
`self_deploy._state_dir`): `armed`, `series` (JSON-список результатов опросов,
|
||||
append каждый тик — restart-safe счётчики), `done`. `read_series`/`append_probe`/
|
||||
`mark_done`/`has_marker` — never-raise.
|
||||
- **`write_post_deploy_log(...)`** — артефакт `16-post-deploy-log.md`, best-effort
|
||||
(по образцу `self_deploy.write_deploy_log`).
|
||||
- **`build_rollback_command(repo)`** — argv хука `--rollback` с прод-env (как
|
||||
`build_deploy_command`, но action=`--rollback`; переиспользует `deploy_prod_*`).
|
||||
|
||||
### 4. Исполнение тика — `stage_engine.run_post_deploy_monitor(job)` + перехват в launcher
|
||||
По образцу `run_deploy_finalizer` / `_run_deploy_finalizer_job`:
|
||||
`launcher.launch_job` перехватывает `agent == "post-deploy-monitor"` ДО `_spawn` →
|
||||
`stage_engine.run_post_deploy_monitor(job)`. Алгоритм тика (never-raise):
|
||||
1. `mark_done` уже стоит → no-op (AC-15, защита от дубля).
|
||||
2. `probe = post_deploy.probe_signals(base_url)`; `append_probe(series, probe)`.
|
||||
3. `verdict = classify(series, ...)`.
|
||||
4. **Если `HEALTHY` и окно не истекло** (число тиков < `window_s/interval_s`) →
|
||||
перепостановка `post-deploy-monitor` через `available_at_delay_s=interval_s`
|
||||
(как finalizer defer; счётчик тиков — из jobs-очереди/`series`, restart-safe).
|
||||
5. **Если `HEALTHY` и окно истекло** → исход `NONE`, `write_post_deploy_log(HEALTHY,
|
||||
NONE)`, `mark_done`, нотификация «окно завершилось чисто» (BR-6/AC-17).
|
||||
6. **Если `DEGRADED`** → `action = decide_action(...)`; исполнить реакцию (§5),
|
||||
`write_post_deploy_log`, `mark_done`, нотификации.
|
||||
|
||||
`mark_done` + sentinel `armed` дают идемпотентность; jobs-очередь +
|
||||
`requeue_running_jobs` + `series` дают restart-safe (AC-15). Бюджет тиков bounded
|
||||
(`window_s/interval_s`) — анти-livelock, как `deploy_finalize_max_attempts`.
|
||||
|
||||
### 5. Реакция на деградацию
|
||||
- **Self-hosting (`orchestrator`), всегда (BR-5/AC-8):** `ALERT_ONLY`. НЕ откатывать
|
||||
и НЕ рестартить прод-контейнер в тике. Громкий Telegram + Plane-коммент с запросом
|
||||
ручного approve отката (по образцу deploy Phase A CTA). `action_taken: ALERT_ONLY`.
|
||||
Откат самого прод-орка (если оператор решит) — ТОЛЬКО через detached host-процесс
|
||||
(контейнер не откатит себя, умирая); переиспользуется механика
|
||||
`self_deploy.initiate_deploy`, но в MVP она вне тика наблюдения (ручной approve →
|
||||
отдельный путь, как ORCH-54 для авто-deploy). Тик self НИКОГДА не запускает хук
|
||||
`--rollback` (структурный инвариант).
|
||||
- **Не-self + `post_deploy_auto_rollback=True` (AC-7):** вызвать хук `--rollback` с
|
||||
прод-env (`build_rollback_command`). Маппинг exit-code по смыслу
|
||||
`map_exit_code_to_status`: `0 → ROLLBACK_OK`; `1/2 → ROLLBACK_FAILED` + громкий
|
||||
Telegram о необходимости ручного вмешательства (AC-9). Целевой контейнер не есть
|
||||
orchestrator → его рестарт безопасен для конвейера.
|
||||
- **Не-self + auto_rollback=False (дефолт):** `ALERT_ONLY`.
|
||||
|
||||
### 6. Артефакт `16-post-deploy-log.md` (новый, машиночитаемый)
|
||||
YAML-frontmatter (канон гейтов; для петли уроков ORCH-8, BR-10):
|
||||
```
|
||||
---
|
||||
post_deploy_status: HEALTHY | DEGRADED
|
||||
action_taken: NONE | ROLLBACK_OK | ROLLBACK_FAILED | ALERT_ONLY
|
||||
work_item: <plane-id>
|
||||
window_s: <int>
|
||||
checks_total: <int>
|
||||
checks_failed: <int>
|
||||
---
|
||||
```
|
||||
Тело — человекочитаемая сводка опросов. Best-effort (отсутствие файла ничего не роняет,
|
||||
AC-13). **Не** читается ни одним гейтом — наблюдение происходит после `done`.
|
||||
|
||||
### 7. Конфигурация — `src/config.py` (env-префикс `ORCH_`)
|
||||
- `post_deploy_monitor_enabled: bool = True` — глобальный kill-switch (BR-8/AC-10).
|
||||
- `post_deploy_repos: str = ""` — CSV применимых репо; пусто → только self-hosting.
|
||||
- `post_deploy_window_s: int = 900` — окно наблюдения (~15 мин, BR-1).
|
||||
- `post_deploy_interval_s: int = 30` — интервал опросов.
|
||||
- `post_deploy_fail_threshold: int = 3` — N послед. провалов health → DEGRADED.
|
||||
- `post_deploy_5xx_threshold: float = 0.5` — порог доли 5xx → DEGRADED.
|
||||
- `post_deploy_auto_rollback: bool = False` — глоб. разрешение авто-отката (для self
|
||||
всегда требует approve, BR-5).
|
||||
- `post_deploy_base_url: str = "http://localhost:8500"` — наблюдаемый прод.
|
||||
- Параметры отката — переиспользовать существующие `deploy_prod_*` (новых дублей нет).
|
||||
|
||||
### 8. Наблюдаемость — блок `post_deploy` в `GET /queue` (BR-9/AC-14)
|
||||
По образцу блока `reconcile` (метод `status()`): `enabled`, `window_s`, `interval_s`,
|
||||
активные наблюдения (по sentinel `armed` без `done`), последний исход
|
||||
(`post_deploy_status`/`action_taken`). Best-effort, never-raise.
|
||||
|
||||
### Инварианты (НЕ меняются)
|
||||
`STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`,
|
||||
момент вердикта `deploy_status`, БАГ-8 откат, terminal-sync `deploy → done`, merge-gate,
|
||||
exit-code-контракт хука (0/1/2), схема БД. Условность как ORCH-35/36/43/58. Never-raise
|
||||
во всём наблюдении (AC-16). Тик self НИКОГДА не рестартит прод-контейнер (AC-8).
|
||||
|
||||
## Альтернативы
|
||||
- **Daemon-watchdog (как reconciler)** — отклонён: per-task серия в памяти не
|
||||
restart-safe; restart-safe-вариант требует тех же sentinel-счётчиков → reserved-agent
|
||||
проще и уже проверен.
|
||||
- **Отдельная пост-deploy стадия + QG** — отклонён: меняет реестры (AC-12), ломает
|
||||
семантику терминального `done`; наблюдение принципиально ПОСЛЕ `done`.
|
||||
- **Авто-rollback прод-орка из тика** — отклонён (BR-5): контейнер не откатит себя
|
||||
надёжно; групповой риск для всех проектов. Self → только ALERT + ручной approve.
|
||||
- **Новая колонка в `tasks` для отметки наблюдения** — отклонён: миграция на проде
|
||||
(риск, как в adr-0007); sentinel-файлы достаточны и restart-safe (как ORCH-36/53/58).
|
||||
- **Прометей/APM** — вне рамок (BR out-of-scope): опираемся на существующие
|
||||
HTTP-эндпоинты, не вводим сбор метрик.
|
||||
|
||||
## Последствия
|
||||
- Класс «зелёный деплой, красный прод» закрыт измеримыми порогами; деградация —
|
||||
машиночитаемый сигнал для петли уроков (ORCH-8).
|
||||
- Плюс: максимальное переиспользование проверенной finalizer/sentinel/hook-машинерии;
|
||||
нулевая миграция БД; реестры не тронуты; дефолты безопасны (auto-rollback off, self
|
||||
только alert).
|
||||
- Минус/ограничение: монитор self бежит ВНУТРИ наблюдаемого прод-контейнера — если
|
||||
контейнер полностью wedged, worker может не выполнить тик и алерта не будет (gap).
|
||||
Это known limitation MVP; внешний независимый watchdog — follow-up (вне рамок).
|
||||
- Минус: каждый тик на короткое время занимает single-worker (`max_concurrency=1`);
|
||||
митигируется коротким опросом (~секунды) и `interval_s` между тиками (defer не держит
|
||||
worker), как finalizer.
|
||||
- Доменный smoke результата фичи (BR-11) — follow-up; MVP = health + 5xx.
|
||||
|
||||
## Связи
|
||||
- **ET-8** — обоснование (deploy SUCCESS, прод не работает).
|
||||
- **adr-0007-executable-self-deploy** (ORCH-36) — sentinel-паттерн, detached
|
||||
host-процесс, `map_exit_code_to_status`, deploy-finalizer reserved-agent (образец).
|
||||
- **adr-0007-reconciler** (ORCH-53) — daemon/`status()` образец (рассмотрен и отклонён
|
||||
как основной механизм; `status()`-снимок в `/queue` переиспользуется).
|
||||
- **adr-0006-merge-gate** / **adr-0003-staging-gate** — образец условности и флагов
|
||||
раската (`*_enabled` + `*_repos`).
|
||||
- **adr-0008-staging-image-provenance** — `.deploy-prev-image` / хук-механика отката.
|
||||
- **ORCH-8** — петля уроков (потребитель `16-post-deploy-log.md`).
|
||||
- **ORCH-54** — будущий полный авто (включая авто-approve отката self), по аналогии
|
||||
с авто-deploy.
|
||||
56
docs/work-items/ORCH-021/07-infra-requirements.md
Normal file
56
docs/work-items/ORCH-021/07-infra-requirements.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 07 — Инфраструктурные требования (ORCH-021)
|
||||
|
||||
> Топология НЕ меняется. Фича опирается на уже существующие HTTP-эндпоинты прода и
|
||||
> существующий деплой-хук. Этот документ фиксирует, какие инфра-предпосылки должны
|
||||
> выполняться, чтобы наблюдение и реакция работали.
|
||||
|
||||
## 1. Топология — без изменений
|
||||
- Прод `orchestrator` (8500), staging `orchestrator-staging` (8501), один сервер
|
||||
mva154 (см. `docs/operations/INFRA.md`). Новых контейнеров/портов/сервисов нет.
|
||||
- Наблюдение — внутрипроцессный reserved-agent job в worker'е прод-контейнера.
|
||||
Daemon-потоков не добавляется (в отличие от reconciler).
|
||||
|
||||
## 2. Наблюдаемый прод — HTTP-эндпоинты
|
||||
- Монитор опрашивает `post_deploy_base_url` (дефолт `http://localhost:8500`):
|
||||
- `GET /health` → ожидается HTTP 200 + тело `{"status":"ok"}` (BR-2);
|
||||
- `GET /status`, `GET /queue` → учёт доли HTTP 5xx (BR-2).
|
||||
- Эндпоинты уже существуют (`src/main.py`). Новых эндпоинтов фича НЕ вводит
|
||||
(out-of-scope APM/метрики).
|
||||
- Для self-hosting `base_url=localhost:8500` означает: монитор бьёт по собственному
|
||||
контейнеру. Это допустимо для MVP (см. риск R-1 в `10-tech-risks.md`).
|
||||
|
||||
## 3. Деплой-хук `--rollback` — предпосылки реакции
|
||||
- Реакция ROLLBACK (только не-self + `post_deploy_auto_rollback=True`) вызывает
|
||||
`scripts/orchestrator-deploy-hook.sh --rollback` с прод-env (переиспользуются
|
||||
`deploy_prod_*`: `TARGET_SERVICE`/`TARGET_PORT`/`TARGET_IMAGE`/`COMPOSE_PROFILE`/
|
||||
`PREV_IMAGE_FILE`), по образцу `self_deploy.build_deploy_command`.
|
||||
- Предпосылка: при штатном деплое хук сохраняет предыдущий образ в
|
||||
`PREV_IMAGE_FILE` (`.deploy-prev-image-prod`). Без снимка → хук вернёт exit 1
|
||||
(«нет prev-образа») → `ROLLBACK_FAILED` + алерт (AC-9). Контракт exit-кодов хука
|
||||
(0/1/2) НЕ меняется.
|
||||
- **Self-hosting:** откат прод-орка хуком в тике ЗАПРЕЩЁН (контейнер не откатит себя,
|
||||
умирая). Если оператор по алерту решит откатить — только detached host-процесс
|
||||
(ssh + setsid, механика `self_deploy.initiate_deploy`), как у Phase B самодеплоя.
|
||||
Предпосылки для detached-пути (ssh-доступ host, shared-mount state-dir) уже
|
||||
выполнены для ORCH-36; в MVP detached-откат self вне тика наблюдения.
|
||||
|
||||
## 4. Restart-safe состояние — shared mount
|
||||
- Состояние наблюдения — sentinel-файлы под `.post-deploy-state-<repo>/<wi>/`
|
||||
(`armed`, `series`, `done`) на том же mount `settings.repos_dir`, что и
|
||||
`.deploy-state-*` (ORCH-36). Миграции БД нет (см. `08-data-requirements.md`).
|
||||
- `requeue_running_jobs` (ORCH-1) восстанавливает claimed `post-deploy-monitor` job
|
||||
после рестарта; `series` хранит счётчики опросов → наблюдение продолжается
|
||||
с того же места (BR-7/AC-15).
|
||||
|
||||
## 5. Конфигурация окружения (env `ORCH_*`)
|
||||
Новые ключи (дефолты безопасны, в `.env`/`.env.staging` по необходимости):
|
||||
`post_deploy_monitor_enabled` (kill-switch, дефолт true), `post_deploy_repos` (CSV,
|
||||
пусто → self-hosting), `post_deploy_window_s` (900), `post_deploy_interval_s` (30),
|
||||
`post_deploy_fail_threshold` (3), `post_deploy_5xx_threshold` (0.5),
|
||||
`post_deploy_auto_rollback` (false), `post_deploy_base_url` (localhost:8500).
|
||||
Параметры отката — существующие `deploy_prod_*`, новых дублей не вводить.
|
||||
|
||||
## 6. Чего НЕ требуется
|
||||
- Новых контейнеров, портов, сетевых правил, секретов.
|
||||
- Prometheus / Grafana / APM (out-of-scope).
|
||||
- Изменений compose-топологии или деплой-пути не-self репо.
|
||||
40
docs/work-items/ORCH-021/08-data-requirements.md
Normal file
40
docs/work-items/ORCH-021/08-data-requirements.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 08 — Требования к данным / схеме БД (ORCH-021)
|
||||
|
||||
## Вывод: миграция БД НЕ требуется
|
||||
Состояние наблюдения хранится в **sentinel-файлах** (restart-safe, без миграции —
|
||||
по образцу ORCH-36/53/58), а не в таблицах. Реестры и схема не меняются (AC-12).
|
||||
|
||||
## 1. Существующие таблицы — без изменений
|
||||
- `events`, `tasks`, `agent_runs`, `jobs` — структура не меняется.
|
||||
- В `tasks` НЕ вводится колонка статуса/окна наблюдения (намеренно — миграция на
|
||||
проде = риск, как обосновано в adr-0007; альтернатива отклонена в ADR-001 §Альтернативы).
|
||||
|
||||
## 2. Очередь `jobs` — переиспользование, без схемы
|
||||
- `post-deploy-monitor` — новый **job-kind** (значение в существующей колонке
|
||||
`agent`/`task_content`), НЕ новая колонка. Ставится через существующий
|
||||
`enqueue_job(..., available_at_delay_s=...)` (ORCH-1).
|
||||
- Счётчик тиков/деферов восстанавливается из jobs-очереди (как
|
||||
`_deploy_finalize_defer_count` считает по `task_content LIKE`), restart-safe.
|
||||
|
||||
## 3. Sentinel-состояние (файлы, не БД)
|
||||
State-dir `.post-deploy-state-<repo>/<work_item_id>/` на `settings.repos_dir`
|
||||
(по образцу `.deploy-state-*`):
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| `armed` | наблюдение заармлено (идемпотентность арма; калька `INITIATED`) |
|
||||
| `series` | JSON-список результатов опросов (счётчики health-fail / 5xx; restart-safe) |
|
||||
| `done` | наблюдение завершено (защита от повторной обработки) |
|
||||
|
||||
Все обращения — never-raise (по образцу `self_deploy.has_marker`/`write_marker`/
|
||||
`read_result`). Отсутствие/битость файла → консервативный фоллбэк, не исключение.
|
||||
|
||||
## 4. Артефакт `16-post-deploy-log.md` — файл репозитория, не БД
|
||||
Машиночитаемый YAML-frontmatter (`post_deploy_status`, `action_taken`, `window_s`,
|
||||
`checks_total`, `checks_failed`) пишется best-effort в `docs/work-items/<id>/`; в БД
|
||||
не реплицируется. Источник для петли уроков ORCH-8 (BR-10).
|
||||
|
||||
## 5. Очистка состояния
|
||||
По завершении окна / реакции `done`-маркер ставится; state-dir можно чистить
|
||||
best-effort (по образцу `self_deploy.clear_state`) — необязательно для корректности,
|
||||
но желательно для гигиены. Stale-`armed` без `done` после краха → виден в `/queue`
|
||||
как «активное наблюдение» и доигрывается восстановленным job'ом.
|
||||
20
docs/work-items/ORCH-021/10-tech-risks.md
Normal file
20
docs/work-items/ORCH-021/10-tech-risks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 10 — Технические риски (ORCH-021)
|
||||
|
||||
| # | Риск | Вероятн. | Влияние | Митигация |
|
||||
|---|------|----------|---------|-----------|
|
||||
| R-1 | **Монитор self бежит внутри наблюдаемого прода.** Полностью wedged прод-контейнер → worker не выполнит тик → деградация не замечена, алерта нет. | Сред. | Высок. | Known MVP limitation (зафиксировано в ADR-001 §Последствия). Health в момент рестарта (хук) + reconciler ловят часть случаев. Внешний независимый watchdog — follow-up (вне рамок). |
|
||||
| R-2 | **Ложный авто-rollback** по сетевому глюку. | Низк. | Высок. | Пороги по N ПОСЛЕДОВАТЕЛЬНЫХ провалов + доля 5xx на окне (BR-3/AC-6), а не разовый провал. Self ВСЕГДА `ALERT_ONLY` (BR-5). `auto_rollback=False` по умолчанию. |
|
||||
| R-3 | **Авто-rollback прод-орка убивает инструмент всех проектов.** | Низк. | Критич. | Структурный инвариант: тик self НИКОГДА не откатывает/рестартит прод-контейнер (AC-8). Self → только alert + ручной approve. Откат self — только detached host-процесс вне тика. |
|
||||
| R-4 | **Нет prev-образа** при ROLLBACK → откат невозможен. | Сред. | Сред. | Хук возвращает exit 1 → `ROLLBACK_FAILED` + громкий алерт (AC-9), деградация не проглатывается тихо. |
|
||||
| R-5 | **Дубль/потеря наблюдения** при двойном webhook / рестарте. | Сред. | Сред. | Идемпотентность: sentinel `armed` (арм-гард) + `done` (защита от повторной обработки) + restart-safe jobs-очередь + `series` (AC-15). По образцу finalizer. |
|
||||
| R-6 | **Исключение в наблюдении роняет worker / конвейер других проектов.** | Низк. | Высок. | Контракт never-raise во всём `post_deploy.py` и `run_post_deploy_monitor` (AC-16), по образцу `self_deploy`/`staging_verdict`. |
|
||||
| R-7 | **Тик занимает single-worker** (`max_concurrency=1`) → задержка других задач. | Низк. | Низк. | Опрос короткий (~секунды), между тиками job не выполняется (defer через `available_at_delay_s`) — worker свободен, как у finalizer. Окно bounded (`window_s/interval_s`). |
|
||||
| R-8 | **Скрытое изменение контракта** (реестры/гейты/exit-коды/схема). | Низк. | Высок. | Инвариант: `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_deploy_status`/terminal-sync/merge-gate/exit-коды/схема БД НЕ меняются (AC-12). Существующие тесты deploy/staging/merge-gate должны остаться зелёными. |
|
||||
| R-9 | **5xx на `/queue`/`/status` из-за самого монитора** (рекурсивная нагрузка). | Низк. | Низк. | Интервал `post_deploy_interval_s` (30с) — низкая частота; опрос лёгкий GET. |
|
||||
| R-10 | **Артефакт `16-post-deploy-log.md` не пишется / невалиден** → петля уроков без данных. | Низк. | Низк. | Best-effort запись с валидным frontmatter (AC-13); отсутствие файла ничего не роняет. Парсинг — defensive. |
|
||||
|
||||
## Эскалация
|
||||
- Изменение помечено `arch:major-change` (новая под-компонента `src/post_deploy.py`
|
||||
+ новый reserved-agent job-kind `post-deploy-monitor`).
|
||||
- R-1 (gap наблюдения для wedged self-контейнера) — кандидат на отдельную задачу
|
||||
(внешний watchdog), вне рамок ORCH-021.
|
||||
99
docs/work-items/ORCH-021/12-review.md
Normal file
99
docs/work-items/ORCH-021/12-review.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-021
|
||||
verdict: APPROVED
|
||||
version: 2
|
||||
---
|
||||
|
||||
# Review ORCH-021 — Post-deploy мониторинг прода + реакция на деградацию
|
||||
|
||||
## Summary
|
||||
Реализация продлевает ответственность конвейера ЗА терминальный переход
|
||||
`deploy → done`, закрывая класс инцидентов «зелёный деплой, красный прод» (ET-8).
|
||||
Механизм — детерминированный reserved-agent job `post-deploy-monitor` (вариант B
|
||||
из ADR-001, точная калька `deploy-finalizer`): арм в `stage_engine.advance_stage`
|
||||
(блок `next_stage == "done"`), один тик = один job (перехват в
|
||||
`launcher.launch_job` ДО `_spawn` → `stage_engine.run_post_deploy_monitor`),
|
||||
чистая логика в новом leaf-модуле `src/post_deploy.py` (never-raise).
|
||||
|
||||
Проверены все четыре оси. Реализация соответствует ТЗ (`02-trz.md`), ADR-001 и
|
||||
глобальному adr-0010, удовлетворяет всем критериям приёмки AC-1…AC-18.
|
||||
Документация (golden-source) обновлена в том же PR. Регрессов нет.
|
||||
|
||||
## Соответствие ТЗ
|
||||
- §2.1 `src/post_deploy.py` (leaf, never-raise): `post_deploy_applies`,
|
||||
`probe_signals`, `classify`, `decide_action`, sentinel-state, артефакт,
|
||||
`build_rollback_command` — все на месте. ✅
|
||||
- §2.2 Оркестрация: арм в terminal-блоке + reserved-agent тик с
|
||||
само-перепостановкой через `available_at_delay_s`; restart-safe (sentinel
|
||||
`armed`/`series`/`done` + jobs-очередь). ✅
|
||||
- §2.3 Реакция: non-self+auto → хук `--rollback` (синхронно, целевой ≠ orch);
|
||||
self-hosting → ВСЕГДА `ALERT_ONLY`. ✅
|
||||
- §2.4 Конфигурация: все `post_deploy_*` в `src/config.py`, дефолты безопасны
|
||||
(kill-switch on, auto-rollback off), параметры отката переиспользуют
|
||||
`deploy_prod_*`. ✅
|
||||
- §2.5 Артефакт `16-post-deploy-log.md` с машиночитаемым frontmatter,
|
||||
best-effort. ✅
|
||||
- §2.6 Блок `post_deploy` в `GET /queue`. ✅
|
||||
- §2.7/§2.8/§3 Инварианты: `STAGE_TRANSITIONS`, `QG_CHECKS`,
|
||||
`check_deploy_status`, terminal-sync, merge-gate, exit-code-контракт хука,
|
||||
схема БД — не тронуты (подтверждено зелёным полным прогоном). ✅
|
||||
|
||||
## Соответствие ADR
|
||||
Реализация 1:1 повторяет ADR-001: механизм (reserved-agent, не стадия/не daemon),
|
||||
точки интеграции, пороги BR-3, политика реакции BR-5 (self never auto-rollback —
|
||||
структурный инвариант в `decide_action` + отсутствие вызова `run_rollback` на
|
||||
ALERT_ONLY). Нарушений глобальных ADR не выявлено.
|
||||
|
||||
## Качество кода
|
||||
- Контракт never-raise выдержан во всех публичных функциях и в каждой ветке
|
||||
`run_post_deploy_monitor`; launcher оборачивает тик в доп. guard (AC-16).
|
||||
- `classify` fail-safe → HEALTHY на мусорном входе (ложный DEGRADED опаснее).
|
||||
- Docstrings содержательные, со ссылками на AC/BR.
|
||||
- Условность раската по образцу ORCH-35/36/43/58 (флаг + CSV-репо).
|
||||
|
||||
## Тесты
|
||||
30 тестов ORCH-021 (`tests/test_post_deploy.py`,
|
||||
`tests/test_post_deploy_integration.py`) — содержательные, покрывают
|
||||
классификацию (AC-3..6), self-hosting safety (TC-19 явно проверяет, что хук
|
||||
`--rollback` НЕ вызывается для self — AC-8), idempotency двойного арма (AC-15),
|
||||
kill-switch/условность (AC-2/10/11), exit-code маппинг (AC-9), frontmatter
|
||||
артефакта (AC-13), never-raise (AC-16), `/queue` (AC-14). Полный прогон
|
||||
`pytest tests/` — **701 passed** (регрессов нет, AC-12).
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice to have
|
||||
- [ ] `run_post_deploy_monitor`: в ветке `ALERT_ONLY` для **не-self** репо при
|
||||
`post_deploy_auto_rollback=false` текст алерта упоминает «авто-rollback для
|
||||
self-hosting запрещён (BR-5)», что для не-self случая формулировка не совсем
|
||||
точна (косметика сообщения; на поведение не влияет).
|
||||
- [ ] `write_post_deploy_log` коммитит/пушит артефакт в ветку задачи, которая к
|
||||
моменту наблюдения уже слита/может быть удалена — артефакт может не попасть в
|
||||
`main`. Контракт best-effort соблюдён (never-raise, ничего не роняет); как
|
||||
улучшение наблюдаемости — рассмотреть запись лог-артефакта отдельным путём.
|
||||
|
||||
## Документация
|
||||
Обновлено в том же PR (golden-source, AC-18 — PASS):
|
||||
- `CLAUDE.md` — `16-post-deploy-log.md` добавлен в перечень артефактов;
|
||||
- `docs/architecture/README.md` — раздел «Post-deploy наблюдение прода» + блок
|
||||
`post_deploy` в таблице API `/queue`;
|
||||
- `docs/architecture/adr/adr-0010-post-deploy-monitor.md` — новый сквозной ADR;
|
||||
- `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md` — детальный ADR;
|
||||
- `CHANGELOG.md` — запись в `Added` (+ fix Dockerfile `COPY data/`);
|
||||
- `README.md` / `.env.example` — все `ORCH_POST_DEPLOY_*` env задокументированы.
|
||||
|
||||
Изменение `src/` сопровождено обновлением документации — правило CLAUDE.md №2/№6
|
||||
выполнено.
|
||||
|
||||
## Вердикт
|
||||
Только P3 (nice-to-have) findings, блокеров и must-fix нет → **APPROVED**.
|
||||
82
docs/work-items/ORCH-021/13-test-report.md
Normal file
82
docs/work-items/ORCH-021/13-test-report.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-021
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-021
|
||||
|
||||
Post-deploy наблюдение прода + реакция на деградацию (reserved-agent job
|
||||
`post-deploy-monitor`, leaf-модуль `src/post_deploy.py`).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3 (asyncio mode=AUTO, anyio 4.13.0)
|
||||
- Ветка: feature/ORCH-021-post-deploy-rollback
|
||||
- Дата: 2026-06-07
|
||||
|
||||
## Прогон
|
||||
- `pytest tests/ -v --tb=short` → **701 passed, 1 warning** (Pydantic V2 deprecation, не относится к задаче).
|
||||
- Целевые модули `tests/test_post_deploy.py` + `tests/test_post_deploy_integration.py` → **30 passed**.
|
||||
|
||||
## Smoke-test (read-only, прод 8500)
|
||||
`curl` в окружении недоступен — опрос через `python urllib` (read-only, прод-контейнер не трогается).
|
||||
|
||||
| Эндпоинт | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | 200 `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | 200, активная задача ORCH-021 на стадии `testing` |
|
||||
| `GET /queue` | 200, counts/resilience/reconcile присутствуют |
|
||||
|
||||
> Примечание: блок `post_deploy` в **живом** `/queue` отсутствует — это ожидаемо: прод
|
||||
> сейчас работает на коде ДО ORCH-021 (задача ещё не задеплоена, стадия testing).
|
||||
> Наличие блока (AC-14) проверяется интеграционным тестом TC-20 против кода ветки → PASS.
|
||||
> Smoke-проверка подтверждает живость окружения, не версию ветки.
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Покрывает AC | Тест-функция | Результат |
|
||||
|-------|----------|--------------|--------------|-----------|
|
||||
| TC-01 | HEALTHY: серия без провалов < порога | AC-3 | test_tc01_healthy_no_failures | PASS |
|
||||
| TC-02 | DEGRADED: N посл. провалов health == threshold | AC-4 | test_tc02_degraded_consecutive_health_failures | PASS |
|
||||
| TC-03 | DEGRADED по 5xx при health=200 | AC-5 | test_tc03_degraded_by_5xx_ratio_even_when_health_200 | PASS |
|
||||
| TC-04 | Нет ложного срабатывания: одиночный глюк + восстановление | AC-6 | test_tc04_no_false_trip_single_glitch_then_recovery | PASS |
|
||||
| TC-05 | Пороги из Settings меняют вердикт на тех же данных | AC-11 | test_tc05_thresholds_change_verdict_on_same_data, test_classify_uses_settings_thresholds | PASS |
|
||||
| TC-06 | не-self + auto_rollback=True + DEGRADED → ROLLBACK | AC-7 | test_tc06_nonself_auto_rollback_degraded_rolls_back | PASS |
|
||||
| TC-07 | self-hosting + DEGRADED → ALERT_ONLY (никогда не авто-rollback) | AC-8 | test_tc07_self_hosting_degraded_never_rolls_back | PASS |
|
||||
| TC-08 | HEALTHY → NONE для любого репо | AC-3 | test_tc08_healthy_means_none_for_any_repo, test_nonself_default_policy_alert_only | PASS |
|
||||
| TC-09 | post_deploy_applies: пусто → только orchestrator | AC-2 | test_tc09_applies_empty_repos_only_self_hosting, test_tc09_applies_explicit_repos_csv | PASS |
|
||||
| TC-10 | kill-switch: monitor_enabled=False → applies()=False для всех | AC-10 | test_tc10_kill_switch_disables_for_everyone | PASS |
|
||||
| TC-11 | Откат exit 0 → ROLLBACK_OK | AC-7 | test_tc11_rollback_exit0_is_ok | PASS |
|
||||
| TC-12 | Откат exit 1/2 → ROLLBACK_FAILED + эскалация | AC-9 | test_tc12_rollback_exit_nonzero_is_failed | PASS |
|
||||
| TC-13 | 16-post-deploy-log.md: валидный YAML-frontmatter | AC-13 | test_tc13_log_frontmatter_parses | PASS |
|
||||
| TC-14 | Опрос при сетевой ошибке → консервативный, не raise | AC-16 | test_tc14_probe_network_error_is_conservative_not_raise, test_tc14_classify_junk_input_swallowed | PASS |
|
||||
| TC-15 | Ошибка записи артефакта → False, не raise | AC-16, AC-13 | test_tc15_write_log_no_worktree_returns_false | PASS |
|
||||
| TC-16 | advance_stage deploy→done армит наблюдение (self), не армит (non-self) | AC-1, AC-2 | test_tc16_arm_for_self_hosting, test_tc16_no_arm_for_nonself, test_tc16_no_arm_when_kill_switch_off | PASS |
|
||||
| TC-17 | Идемпотентность: повторный арм не задваивает | AC-15 | test_tc17_double_arm_is_noop | PASS |
|
||||
| TC-18 | Полный цикл DEGRADED → не-self откат + лог + уведомление | AC-7, AC-13, AC-17 | test_tc18_degraded_nonself_rolls_back | PASS |
|
||||
| TC-19 | Self-hosting DEGRADED → НЕ рестарт/откат, алерт+approve | AC-8, AC-17 | test_tc19_degraded_self_hosting_alert_only | PASS |
|
||||
| TC-20 | GET /queue содержит блок post_deploy | AC-14 | test_tc20_queue_block_present | PASS |
|
||||
| TC-21 | Регресс: deploy/staging/merge-gate/reconciler зелёные; STAGE_TRANSITIONS/QG_CHECKS не изменены | AC-12 | tests/test_stages.py (+ полный прогон 701) | PASS |
|
||||
|
||||
Доп. тесты ветки (не из плана, подтверждают контракты): `test_series_append_and_read_roundtrip`,
|
||||
`test_mark_done_idempotency_marker`, `test_healthy_tick_requeues_without_finishing`,
|
||||
`test_finished_window_tick_is_noop` — все PASS.
|
||||
|
||||
## Покрытие критериев приёмки
|
||||
AC-1…AC-18 — все покрыты прошедшими тестами (см. таблицу). AC-12 (реестры/схема БД
|
||||
не изменены) дополнительно подтверждён зелёным полным регрессом 701 теста, включая
|
||||
deploy/staging/merge-gate/reconciler. AC-18 (документация) — вне scope прогона тестов,
|
||||
подтверждён ревью (12-review.md, verdict APPROVED).
|
||||
|
||||
## Вывод pytest (хвост)
|
||||
```
|
||||
======================= 701 passed, 1 warning in 12.71s ========================
|
||||
```
|
||||
```
|
||||
======================== 30 passed, 1 warning in 0.58s =========================
|
||||
```
|
||||
|
||||
## Итог
|
||||
**PASS.** Все 21 тест-кейс плана зелёные, полный регресс (701) зелёный, smoke прод-эндпоинтов
|
||||
OK (окружение живо). Существующие контракты не сломаны. Задача готова к стадии deploy-staging.
|
||||
42
docs/work-items/ORCH-021/15-staging-log.md
Normal file
42
docs/work-items/ORCH-021/15-staging-log.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T14:37:33Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed. Verdict: **SUCCESS** (exit 0).
|
||||
|
||||
Run canonically inside the `orchestrator-staging` container (ORCH-048, ADR-001)
|
||||
via the Docker Engine API over the mounted socket (`docker` CLI is not installed
|
||||
in the prod-agent container; `network_mode: host` + group `999` allow direct
|
||||
socket access):
|
||||
|
||||
```
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
```
|
||||
RESULT: 8/10 checks PASS
|
||||
REAL failed : none
|
||||
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
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
|
||||
```
|
||||
|
||||
- **Block A (SMOKE):** A1 `/health` 200 ok, A2 `/queue` 200, A3 `ORCH_STAGING=true` — all PASS.
|
||||
- **Block B (ACCESS):** B4 Plane sandbox, B5 Gitea `orchestrator-sandbox` (push=true),
|
||||
B6 registry isolation (sandbox present, prod ET/ORCH absent) — all PASS.
|
||||
- **Block C (E2E, stub):** C7 create issue in SANDBOX, C8 trigger pipeline via
|
||||
`/webhook/plane` — PASS. C9a/C9b FAILED but are sandbox-infra checks (bot accounts
|
||||
not members of the SANDBOX Plane project) — **waived** per ORCH-061; not a pipeline
|
||||
regression. Cleanup deleted the test Plane issue (HTTP 204).
|
||||
|
||||
All REAL pipeline checks are green; the only failures are the two known
|
||||
sandbox-infra checks, which the verdict tolerates (`staging_infra_tolerance_enabled=true`).
|
||||
The script exited 0 → advance.
|
||||
7
docs/work-items/ORCH-022/00-business-request.md
Normal file
7
docs/work-items/ORCH-022/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: [★ высокий] Security-гейт: secret-scanning + аудит зависимостей перед мержем
|
||||
|
||||
Work Item ID: ORCH-022
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
150
docs/work-items/ORCH-022/01-brd.md
Normal file
150
docs/work-items/ORCH-022/01-brd.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 01 — BRD: Security-гейт (secret-scanning + аудит зависимостей перед мержем)
|
||||
|
||||
Work Item: **ORCH-022**
|
||||
Приоритет: **★ высокий**
|
||||
Источник: предложение Стрим, одобрено Славой (2026-06-04).
|
||||
Стадия: analysis.
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-проблема
|
||||
|
||||
Оркестратор — автономная мульти-агентная система: агенты (`developer`) пишут код
|
||||
**без человека-фильтра по умолчанию**. Перед мержем в `main` сейчас нет проверки на:
|
||||
|
||||
- **утёкший секрет** — закоммиченный API-ключ / токен / пароль / приватный ключ;
|
||||
- **дырявую зависимость** — пакет с известной CVE;
|
||||
- (опционально) **базовую уязвимость кода** — типовой SAST-паттерн.
|
||||
|
||||
Для автономной системы это критично: ошибку, которую в обычной команде «выловили бы
|
||||
глазами на ревью», здесь поймать некому. Утёкший в `git`-историю ключ или уязвимая
|
||||
зависимость может уехать в прод и обслуживать **все** проекты (общий инстанс,
|
||||
self-hosting).
|
||||
|
||||
### Прецеденты / связки
|
||||
- **PR #18** (`check_ci_green`: красный CI → возврат на `development`) — задаёт целевой
|
||||
паттерн поведения красного гейта. Security-гейт должен вести себя так же.
|
||||
- **Управление секретами** (CLAUDE.md §8): секреты живут только в `.env`/`.env.staging`
|
||||
на хосте, канон — `.env.example`. Гейт — это автоматический страж этого правила.
|
||||
|
||||
---
|
||||
|
||||
## 2. Цель
|
||||
|
||||
Ввести **security-гейт перед слиянием ветки задачи в `main`**, который детерминированно
|
||||
(без LLM) проверяет diff/ветку на секреты и уязвимые зависимости и **блокирует
|
||||
продвижение** при нарушении порогов: красный security-гейт → **возврат на `development`**
|
||||
(developer-retry, как красный CI / merge-gate), задача **не уезжает в прод**.
|
||||
|
||||
### Бизнес-ценность
|
||||
- Структурно невозможно «тихо» влить секрет или известную CVE в прод автономной системы.
|
||||
- Самоприменение правила CLAUDE.md §8 (секреты не в гит) без участия человека.
|
||||
- Расширяет уже выстроенную линию автономных страховок (CI-гейт, merge-gate ORCH-043,
|
||||
staging-провенанс ORCH-058, post-deploy ORCH-021).
|
||||
|
||||
---
|
||||
|
||||
## 3. Объём (Scope)
|
||||
|
||||
### 3.1 В объёме (v1) — **предположение по умолчанию (A1)**
|
||||
1. **Secret-scanning** — обязательный минимум гейта. Поиск закоммиченных секретов
|
||||
в ветке задачи / её diff относительно `main`.
|
||||
2. **Dependency audit** — аудит зависимостей проекта на известные CVE.
|
||||
3. **Машиночитаемый артефакт-вердикт** security-гейта (YAML-frontmatter — канон гейтов).
|
||||
4. **Поведение красного гейта** = откат на `development` + developer-retry (cap
|
||||
`MAX_DEVELOPER_RETRIES = 3`), наблюдаемость (Telegram + Plane-коммент).
|
||||
5. **Условный раскат** (kill-switch + scope репозиториев), **never-raise**,
|
||||
self-hosting (`orchestrator`) — первым.
|
||||
|
||||
### 3.2 Вне объёма (v1) — **предположение (A2), отдельные WI**
|
||||
- **SAST (semgrep)** — вынесен в follow-up WI: шумнее, требует policy-тюнинга правил;
|
||||
гейт проектируется с точкой расширения под него, но в v1 не включается.
|
||||
- **Полноценный мульти-стек** (JS/npm, Android) — см. A3 ниже; в v1 целевой стек —
|
||||
Python (сам оркестратор). Связь с ORCH-9/15 фиксируется как зависимость на будущее.
|
||||
- Ретроспективное сканирование уже существующей истории `main` (гейт смотрит вперёд —
|
||||
ветку перед мержем, не чистит прошлое).
|
||||
- Управление аллоулистом ложных срабатываний через UI/Plane (в v1 — файл в репозитории).
|
||||
|
||||
### 3.3 Зафиксированные предположения по умолчанию
|
||||
> ⚠️ Интерактивный опрос Owner на стадии анализа не дал ответа; ниже —
|
||||
> **дефолты по конвенциям проекта**. Любой из них Owner/архитектор может переопределить
|
||||
> (для A4 предусмотрены конфиг-флаги порогов).
|
||||
|
||||
- **A1 (объём сканеров v1):** secret-scanning + dependency-audit. SAST отложен.
|
||||
- **A2 (SAST):** отложен в отдельный WI; гейт оставляет точку расширения.
|
||||
- **A3 (стек):** **Python-only сначала**, реально только для self-hosting
|
||||
(`is_self_hosting_repo` / scope-CSV), как ORCH-35/43/58. Прочие репо — no-op pass.
|
||||
Мульти-стек (детект стека по репо) — отдельный WI.
|
||||
- **A4 (пороги):** **секреты — всегда блок**; **зависимости — блок на HIGH/CRITICAL,
|
||||
warning на MEDIUM/LOW**. Пороги вынесены в конфиг (переопределяемы без редеплоя кода).
|
||||
|
||||
---
|
||||
|
||||
## 4. Заинтересованные стороны
|
||||
| Роль | Интерес |
|
||||
|------|---------|
|
||||
| Owner (Слава) | Прод-безопасность автономного конвейера; контроль порогов и раската. |
|
||||
| Стрим | Инициатор; снижение риска утечки/уязвимости в автономном режиме. |
|
||||
| Агент `developer` | Получает понятную причину красного гейта → быстрый фикс. |
|
||||
| Агент `reviewer` | Гейт снимает с него непосильную задачу «глазами ловить ключи». |
|
||||
| Все проекты на инстансе | Общий прод не должен получить секрет/CVE через одну задачу. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Бизнес-требования
|
||||
|
||||
| ID | Требование | Приоритет |
|
||||
|----|-----------|-----------|
|
||||
| BR-1 | Перед слиянием ветки задачи в `main` обязателен security-гейт (секреты + аудит зависимостей). | MUST |
|
||||
| BR-2 | Найден секрет (порог A4) → гейт **красный** → откат на `development`, в прод не уходит. | MUST |
|
||||
| BR-3 | Уязвимость зависимости уровня блокировки (порог A4) → гейт **красный** → откат на `development`. | MUST |
|
||||
| BR-4 | Уязвимость ниже порога блокировки → **warning**, продвижение не блокируется, но фиксируется в артефакте. | MUST |
|
||||
| BR-5 | Красный гейт ведёт себя как красный CI / merge-gate: откат на `development` + developer-retry (cap 3), затем эскалация (Telegram + Plane Blocked). | MUST |
|
||||
| BR-6 | Вердикт гейта — **машиночитаемый** (YAML-frontmatter артефакта), читается гейтом ТОЛЬКО из frontmatter (канон проекта), не из прозы. | MUST |
|
||||
| BR-7 | Гейт **детерминированный, без LLM** в критическом пути (как merge-gate / image-freshness). | MUST |
|
||||
| BR-8 | Гейт **never-raise**: внутренняя ошибка не роняет `advance_stage` и не вешает конвейер всех проектов. | MUST |
|
||||
| BR-9 | Условный раскат: глобальный kill-switch + scope-CSV репозиториев; пусто → реально только self-hosting (`orchestrator`), прочие репо — no-op pass. | MUST |
|
||||
| BR-10 | Пороги блокировки конфигурируемы (env-флаги, без редеплоя кода). | SHOULD |
|
||||
| BR-11 | Наблюдаемость: причина блокировки видна (Telegram + Plane-коммент + артефакт); проход — без шума. | MUST |
|
||||
| BR-12 | Документация (CLAUDE.md «Артефакты задачи», `docs/architecture/README.md` таблица гейтов, CHANGELOG, ADR) обновлена в том же PR. | MUST |
|
||||
| BR-13 | Аллоулист ложных срабатываний (заведомо-безопасные совпадения, напр. в `.env.example`, фикстуры тестов) поддерживается версионируемым файлом в репозитории. | SHOULD |
|
||||
| BR-14 | Точка расширения под SAST и мульти-стек заложена, но в v1 не активна (A2/A3). | SHOULD |
|
||||
|
||||
---
|
||||
|
||||
## 6. Ограничения и риски (бизнес-уровень)
|
||||
- **Self-hosting:** гейт исполняется внутри инстанса, который правит сам себя. Запрет на
|
||||
рестарт/падение прод-контейнера в рамках задачи (CLAUDE.md §self-hosting) сохраняется —
|
||||
гейт ничего не деплоит и не рестартит, только читает/сканирует.
|
||||
- **Ложные срабатывания** (false positives) могут зациклить откат `→ development`
|
||||
(прецедент ORCH-061 со staging-петлёй). Митигировано: cap retry=3 + аллоулист (BR-13)
|
||||
+ конфигурируемые пороги (BR-10) + kill-switch (BR-9).
|
||||
- **Внешние БД уязвимостей** (CVE-фиды) — сетевая зависимость; недоступность фида не
|
||||
должна давать ложный красный (см. AC: degrade-поведение при недоступности фида —
|
||||
решение порога «fail-open vs fail-closed для аудита» закрепляется в acceptance + ADR).
|
||||
- **Стоимость/время** сканирования добавляется к каждому прогону задачи — должно быть
|
||||
ограничено таймаутом (как merge-retest).
|
||||
|
||||
---
|
||||
|
||||
## 7. Критерий успеха (бизнес)
|
||||
Ветка с подсаженным тестовым секретом и/или зависимостью с известной CRITICAL-CVE
|
||||
**не может** дойти до `main`/прода: гейт краснеет, задача откатывается на `development`
|
||||
с понятной причиной. Чистая ветка проходит гейт без задержек и без шума. Для не-self
|
||||
репозиториев конвейер не меняется (no-op). Прод-контейнер не рестартится гейтом.
|
||||
|
||||
---
|
||||
|
||||
## 8. Открытые вопросы (для архитектора / Owner)
|
||||
1. **Размещение гейта** (решение архитектора): (а) на стадии `review`, либо (б) отдельный
|
||||
под-гейт перед мержем на ребре `deploy-staging → deploy` (где уже живёт merge-gate
|
||||
ORCH-043 / image-freshness ORCH-058). Требование BRD — «перед слиянием в `main`»;
|
||||
обе опции его удовлетворяют. См. 02-trz §4.
|
||||
2. **Где запускается сканер**: новый job в `.gitea/workflows/ci.yml` (тогда вердикт может
|
||||
течь через существующий `check_ci_green`) **или** отдельный QG-чек/под-гейт в `src/qg`.
|
||||
Решение — архитектор (02-trz фиксирует требования к обоим путям).
|
||||
3. **Аудит зависимостей при недоступном CVE-фиде:** fail-open (warning) или fail-closed
|
||||
(блок)? Дефолт-предложение — **fail-open с громким warning** (не плодить ложные
|
||||
завороты), закрепить в ADR.
|
||||
4. **Выбор конкретных инструментов** (gitleaks vs trufflehog; pip-audit vs trivy) —
|
||||
технологическое решение архитектора; BRD фиксирует только функцию.
|
||||
175
docs/work-items/ORCH-022/02-trz.md
Normal file
175
docs/work-items/ORCH-022/02-trz.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 02 — ТЗ: Security-гейт (secret-scanning + dependency audit)
|
||||
|
||||
Work Item: **ORCH-022** · Стадия: analysis · См. `01-brd.md`, `03-acceptance-criteria.md`.
|
||||
|
||||
> **Граница ответственности аналитика.** Ниже — *функциональные требования и точки
|
||||
> касания* кода. Выбор размещения гейта в пайплайне, конкретных инструментов и схемы
|
||||
> модулей — **решение архитектора** (см. §4 и `01-brd.md` §8). ТЗ фиксирует требования к
|
||||
> любому из допустимых вариантов и инварианты, которые нельзя нарушать.
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст кода (как есть)
|
||||
|
||||
- **Стадии:** `src/stages.py::STAGE_TRANSITIONS` — линейный конвейер
|
||||
`… review → testing → deploy-staging → deploy → done`. Фактический merge ветки в
|
||||
`main` делает агент `deployer` **в начале стадии `deploy`** (CLAUDE/README).
|
||||
- **Quality Gates:** `src/qg/checks.py` — реестр `QG_CHECKS` (имя → функция), сигнатуры
|
||||
диспетчеризуются в `src/stage_engine.py::_run_qg`.
|
||||
- **Существующий паттерн «красный гейт → возврат developer»:**
|
||||
`check_ci_green` (PR #18) и rollback-ветки в
|
||||
`stage_engine._handle_qg_failure_rollbacks` (откат на `development`, developer-retry,
|
||||
cap `MAX_DEVELOPER_RETRIES = 3`, затем `set_issue_blocked` + Telegram).
|
||||
- **Эталонный паттерн детерминированного под-гейта на ребре** (без LLM, never-raise,
|
||||
условный раскат, откат на `development`):
|
||||
- merge-gate **ORCH-043** — `src/merge_gate.py` + `check_branch_mergeable` +
|
||||
`stage_engine._handle_merge_gate` (ребро `deploy-staging → deploy`);
|
||||
- image-freshness **ORCH-058** — `src/image_freshness.py` + `_check_staging_image_fresh`
|
||||
+ `stage_engine._handle_image_freshness` (то же ребро).
|
||||
Оба: leaf-модуль с чистой логикой (never-raise) + тонкая обёртка в `QG_CHECKS` +
|
||||
врезка-обработчик в `advance_stage`, kill-switch `*_enabled` + scope `*_repos`,
|
||||
реально только для self-hosting при пустом scope.
|
||||
- **CI:** `.gitea/workflows/ci.yml` — один job `test` (pytest) на `self-hosted` раннере,
|
||||
push в `feature/**` и PR в `main`. `check_ci_green` читает комбинированный статус
|
||||
коммита из Gitea API.
|
||||
- **Артефакты задачи** нумерованы до `16-post-deploy-log.md`.
|
||||
- **Зависимости Python:** `requirements.txt` (корень репо).
|
||||
|
||||
---
|
||||
|
||||
## 2. Функциональные требования к реализации
|
||||
|
||||
### FR-1. Secret-scanning ветки перед мержем
|
||||
- Сканировать ветку задачи / её diff относительно `origin/main` на секреты
|
||||
(ключи, токены, пароли, приватные ключи).
|
||||
- **Любой** подтверждённый секрет (не из аллоулиста) → вердикт **FAIL** (порог A4: секреты
|
||||
всегда блокируют).
|
||||
- Инструмент (gitleaks / trufflehog) — выбор архитектора. Должен запускаться offline-/
|
||||
детерминированно (без LLM) и иметь конфиг правил/аллоулиста в репозитории.
|
||||
|
||||
### FR-2. Dependency audit
|
||||
- Аудит зависимостей целевого стека на известные CVE. Для Python — манифест
|
||||
`requirements.txt` (инструмент pip-audit / trivy — выбор архитектора).
|
||||
- Классификация по severity. **Порог блокировки (A4, конфигурируемо BR-10):**
|
||||
- `CRITICAL`, `HIGH` → вклад в **FAIL**;
|
||||
- `MEDIUM`, `LOW` → **warning** (фиксируется в артефакте, не блокирует).
|
||||
- Недоступность CVE-фида: degrade-поведение по решению ADR (дефолт-предложение —
|
||||
fail-open + громкий warning, чтобы не плодить ложные завороты). Поведение должно быть
|
||||
детерминированным и протестированным.
|
||||
|
||||
### FR-3. Машиночитаемый артефакт-вердикт
|
||||
- Гейт порождает артефакт security-отчёта с **YAML-frontmatter**, напр.:
|
||||
```
|
||||
---
|
||||
security_status: PASS # PASS | FAIL
|
||||
secrets_found: 0
|
||||
deps_blocking: 0 # число уязвимостей уровня блокировки
|
||||
deps_warning: 2
|
||||
---
|
||||
```
|
||||
Имя артефакта — предложение: **`17-security-report.md`** (следующий свободный номер;
|
||||
финализирует архитектор). Тело — человекочитаемый список находок.
|
||||
- Вердикт читается гейтом **ТОЛЬКО из frontmatter** (канон проекта: «машинные вердикты —
|
||||
строго YAML-frontmatter, никогда проза»), по образцу `_parse_deploy_status` /
|
||||
`_parse_staging_status` / `check_reviewer_verdict`. Negative-токен (FAIL) авторитетен.
|
||||
- Отсутствие/битый frontmatter → `(False, reason)` (fail-closed на чтении вердикта,
|
||||
как у существующих парсеров).
|
||||
|
||||
### FR-4. Поведение красного гейта (откат)
|
||||
- `security_status: FAIL` → откат на `development` + enqueue `developer`, по образцу
|
||||
`_handle_qg_failure_rollbacks` (merge-gate-ветка — точный шаблон):
|
||||
- cap `MAX_DEVELOPER_RETRIES` (3); при исчерпании — `set_issue_blocked` + Telegram-алерт;
|
||||
- `task_desc` для developer несёт **дословную причину** (какие секреты/CVE), по образцу
|
||||
ORCH-046 (встраивание must-fix в `task_desc`), а не только ссылку на артефакт;
|
||||
- Plane-коммент + `notify_qg_failure` (наблюдаемость BR-11).
|
||||
|
||||
### FR-5. Условный раскат (как ORCH-35/43/58)
|
||||
- Глобальный kill-switch `security_gate_enabled` (env `ORCH_SECURITY_GATE_ENABLED`,
|
||||
дефолт по согласованию; рекомендуется `true` с safety-net, как у соседних фич).
|
||||
- Scope `security_gate_repos` (CSV); пусто → реально только `is_self_hosting_repo(repo)`
|
||||
(`orchestrator`). Прочие репо → `(True, "security-gate N/A for <repo>")` (мгновенный pass).
|
||||
- Отдельные пороги-флаги (A4/BR-10): напр. `security_dep_block_severity`
|
||||
(`HIGH` по умолчанию), при желании `security_secrets_block` (`true`).
|
||||
|
||||
### FR-6. never-raise
|
||||
- Любая внутренняя ошибка гейта (сбой сканера, отсутствие бинаря, таймаут) →
|
||||
`(False, "<reason>")` **без** проброса исключения в `advance_stage`. Контракт —
|
||||
как у `check_branch_mergeable` (внешний + внутренний guard).
|
||||
- Таймаут сканирования ограничен (по образцу `merge_retest_timeout_s`).
|
||||
|
||||
### FR-7. Наблюдаемость
|
||||
- Блокировка → Telegram + Plane-коммент (BR-11). Проход → лог-строка, без шумных
|
||||
нотификаций (по образцу merge-gate pass).
|
||||
- Желательно: краткий снимок в `GET /queue` (опционально, по образцу блоков `reconcile`/
|
||||
`reaper`/`post_deploy`) — на усмотрение архитектора.
|
||||
|
||||
---
|
||||
|
||||
## 3. Задействованные модули `src/` (точки касания)
|
||||
|
||||
| Модуль | Изменение |
|
||||
|--------|-----------|
|
||||
| `src/security_gate.py` (**новый leaf-модуль**) | Чистая логика гейта: запуск сканеров, классификация по severity, применение порогов/аллоулиста, формирование вердикта + парсер frontmatter. **never-raise.** По образцу `src/merge_gate.py` / `src/image_freshness.py` / `src/post_deploy.py`. |
|
||||
| `src/qg/checks.py` | Новый чек `check_security_gate` (тонкая обёртка над `security_gate`, ленивый импорт во избежание циклов) + регистрация в `QG_CHECKS`. Условность (kill-switch/scope/self-hosting) — как `check_branch_mergeable` / `_check_staging_image_fresh`. |
|
||||
| `src/stage_engine.py` | Врезка-обработчик `_handle_security_gate(...)` по образцу `_handle_merge_gate` / `_handle_image_freshness`: вызов в `advance_stage` на выбранном архитектором ребре; FAIL → откат на `development` (FR-4); never-raise. **`STAGE_TRANSITIONS` НЕ меняется**, если выбран вариант «под-гейт ребра». |
|
||||
| `src/config.py` | Новые настройки: `security_gate_enabled`, `security_gate_repos`, `security_dep_block_severity`, `security_scan_timeout_s` (+ при необходимости пути к бинарям/конфигам сканеров). С docstring-комментариями по образцу ORCH-043/058. |
|
||||
| `.gitea/workflows/ci.yml` | **Если** архитектор выберет CI-путь: новый job `security` (secret-scan + dep-audit), влияющий на комбинированный статус коммита (тогда срабатывает `check_ci_green`-паттерн PR #18). Иначе — не трогается. |
|
||||
| `requirements.txt` / Dockerfile | Установка выбранных сканеров (если они Python-пакеты — в `requirements.txt`; если бинари — в Dockerfile/раннер). |
|
||||
| Конфиг сканера + аллоулист | Версионируемые файлы в репозитории (напр. `.gitleaks.toml` / аллоулист) — BR-13. |
|
||||
| `.openclaw/agents/developer.md` | (Если нужно) краткая инструкция developer'у про устранение security-находок при заворотах. |
|
||||
|
||||
> Если выбран вариант «гейт на стадии `review`» — врезка делается в соответствующую
|
||||
> ветку `advance_stage`/обработчик ревью вместо ребра `deploy-staging → deploy`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Размещение в пайплайне — варианты для архитектора
|
||||
|
||||
Требование BRD: **«перед слиянием ветки в `main`»**. Допустимы (выбор + обоснование — в ADR):
|
||||
|
||||
- **Вариант R (review):** security-проверка на стадии `review` (раньше отлов, дешевле
|
||||
откат — задача ещё близко к development). Минус: дальше по конвейеру `main` может уйти
|
||||
вперёд (но это закрывает merge-gate).
|
||||
- **Вариант M (merge-edge, рекомендуемый к рассмотрению):** под-гейт на ребре
|
||||
`deploy-staging → deploy`, рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058) —
|
||||
непосредственно перед фактическим мержем `deployer`'ом. Плюс: единое место «последней
|
||||
страховки перед main», переиспользование готового паттерна врезки/отката/lease.
|
||||
- **Вариант C (CI-job):** добавить job в `ci.yml`; вердикт течёт через `check_ci_green`.
|
||||
Плюс: меньше нового кода в движке. Минус: пороги/severity-логика и артефакт-вердикт
|
||||
сложнее выразить только статусом коммита.
|
||||
|
||||
ТЗ не предписывает вариант; реализация обязана сохранить инварианты §6.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменения API
|
||||
- Новых HTTP-endpoint'ов **не требуется**.
|
||||
- Допустимо (опционально, FR-7): расширить ответ `GET /queue` блоком `security`
|
||||
(counts/last_run) — по образцу блоков `reconcile`/`reaper`/`post_deploy`. Не обязательно.
|
||||
|
||||
## 6. Изменения схемы БД
|
||||
- **Не требуется.** Состояние гейта — артефакт-файл + (при необходимости) sentinel-файлы,
|
||||
по образцу merge-lease / deploy-state / post-deploy-state. Миграций БД нет.
|
||||
- Если архитектор сочтёт нужным считать security-retry отдельно от developer-retry —
|
||||
предпочесть подсчёт по `jobs`/`agent_runs` (как `_developer_retry_count` /
|
||||
`_merge_defer_count`), без новых колонок.
|
||||
|
||||
## 7. Инварианты (НЕ нарушать)
|
||||
1. `STAGE_TRANSITIONS` и реестр `QG_CHECKS` остаются консистентными; при варианте
|
||||
«под-гейт ребра» — `STAGE_TRANSITIONS` не меняется (триггер — то же событие стадии).
|
||||
2. Машинный вердикт — только из YAML-frontmatter, не из прозы.
|
||||
3. never-raise: гейт никогда не пробрасывает исключение в `advance_stage`.
|
||||
4. Условность как ORCH-35/43/58: не-self репо при пустом scope не затрагиваются (no-op).
|
||||
5. Гейт **не деплоит и не рестартит** прод-контейнер (self-hosting safety).
|
||||
6. Откат и retry-счётчик developer не ломаются (cap=3, затем эскалация).
|
||||
7. Документация (CLAUDE.md, README, CHANGELOG, ADR) обновлена в том же PR (BR-12).
|
||||
|
||||
## 8. Артефакты pipeline, создаваемые/обновляемые
|
||||
- **Новый:** `docs/work-items/ORCH-022/17-security-report.md` (имя финализирует архитектор)
|
||||
с `security_status:`-frontmatter (FR-3) — порождается гейтом per-task.
|
||||
- **ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-<slug>.md` (решение: размещение,
|
||||
инструменты, degrade-поведение фида, пороги). При сквозном влиянии — global ADR в
|
||||
`docs/architecture/adr/`.
|
||||
- **Обновить:** `CLAUDE.md` (раздел «Артефакты задачи» — добавить 17-…),
|
||||
`docs/architecture/README.md` (таблица гейтов + реестр `QG_CHECKS` + новый раздел),
|
||||
`CHANGELOG.md`, `.env.example` (новые `ORCH_SECURITY_*`).
|
||||
140
docs/work-items/ORCH-022/03-acceptance-criteria.md
Normal file
140
docs/work-items/ORCH-022/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 03 — Критерии приёмки: Security-гейт (ORCH-022)
|
||||
|
||||
Формат: каждый критерий имеет чёткое условие **PASS/FAIL**. Привязка к
|
||||
`01-brd.md` (BR-*) и `02-trz.md` (FR-*).
|
||||
|
||||
---
|
||||
|
||||
## A. Secret-scanning (FR-1, BR-1/BR-2)
|
||||
|
||||
### AC-1 — Подсаженный секрет блокирует гейт
|
||||
- **PASS:** ветка с тестовым секретом (напр. фиктивный AWS-ключ формата `AKIA…` вне
|
||||
аллоулиста) → `security_status: FAIL`; гейт возвращает `(False, reason)`, причина
|
||||
называет секрет/файл.
|
||||
- **FAIL:** секрет не обнаружен ИЛИ гейт зелёный при наличии секрета.
|
||||
|
||||
### AC-2 — Чистая ветка проходит
|
||||
- **PASS:** ветка без секретов → `security_status: PASS`; `secrets_found: 0`;
|
||||
гейт возвращает `(True, …)`.
|
||||
- **FAIL:** ложное срабатывание (FAIL на чистой ветке).
|
||||
|
||||
### AC-3 — Аллоулист подавляет заведомо-безопасное (BR-13)
|
||||
- **PASS:** совпадение, явно занесённое в версионируемый аллоулист (напр. плейсхолдер в
|
||||
`.env.example` / фикстура теста), **не** даёт FAIL.
|
||||
- **FAIL:** аллоулист игнорируется и даёт ложный FAIL.
|
||||
|
||||
---
|
||||
|
||||
## B. Dependency audit (FR-2, BR-3/BR-4)
|
||||
|
||||
### AC-4 — CVE уровня блокировки краснит гейт
|
||||
- **PASS:** зависимость с известной `CRITICAL`/`HIGH` CVE (при пороге по умолчанию
|
||||
`HIGH`) → вклад в `security_status: FAIL`; `deps_blocking >= 1`.
|
||||
- **FAIL:** блокирующая уязвимость не приводит к FAIL.
|
||||
|
||||
### AC-5 — Низкая severity = warning, не блок
|
||||
- **PASS:** только `MEDIUM`/`LOW` уязвимости → `security_status: PASS`, при этом
|
||||
`deps_warning >= 1` и находки перечислены в теле артефакта.
|
||||
- **FAIL:** `MEDIUM`/`LOW` блокирует продвижение.
|
||||
|
||||
### AC-6 — Порог блокировки конфигурируем (BR-10)
|
||||
- **PASS:** при `ORCH_SECURITY_DEP_BLOCK_SEVERITY=CRITICAL` та же `HIGH`-уязвимость
|
||||
становится warning (не блок); при `=HIGH` — блок. Поведение детерминированно
|
||||
определяется флагом.
|
||||
- **FAIL:** флаг не влияет на классификацию.
|
||||
|
||||
### AC-7 — Degrade при недоступном CVE-фиде
|
||||
- **PASS:** недоступность фида обрабатывается по решению ADR детерминированно и
|
||||
протестированно (дефолт: fail-open + громкий warning, гейт не краснеет ложно).
|
||||
- **FAIL:** недоступность фида даёт неконтролируемый красный/исключение.
|
||||
|
||||
---
|
||||
|
||||
## C. Вердикт и артефакт (FR-3, BR-6)
|
||||
|
||||
### AC-8 — Машинный вердикт только из frontmatter
|
||||
- **PASS:** вердикт читается ТОЛЬКО из YAML-frontmatter `17-security-report.md`; проза с
|
||||
«PASS»/«FAIL» в теле не влияет на решение. Negative-токен (FAIL) авторитетен.
|
||||
- **FAIL:** вердикт извлекается из тела/прозы.
|
||||
|
||||
### AC-9 — Битый/отсутствующий frontmatter → fail-closed на чтении
|
||||
- **PASS:** нет frontmatter / битый YAML / нет поля `security_status` → `(False, reason)`
|
||||
(как `_parse_deploy_status`/`check_reviewer_verdict`).
|
||||
- **FAIL:** битый артефакт трактуется как PASS.
|
||||
|
||||
### AC-10 — Артефакт создаётся с корректными полями
|
||||
- **PASS:** после прогона существует `17-security-report.md` с валидным frontmatter
|
||||
(`security_status`, `secrets_found`, `deps_blocking`, `deps_warning`) и телом-списком.
|
||||
- **FAIL:** артефакт не создан/без машинных полей.
|
||||
|
||||
---
|
||||
|
||||
## D. Откат и retry (FR-4, BR-5)
|
||||
|
||||
### AC-11 — Красный гейт → откат на development + developer-retry
|
||||
- **PASS:** `FAIL` → стадия задачи становится `development`, enqueue `developer`,
|
||||
Plane-коммент + `notify_qg_failure`; счётчик developer-retry растёт.
|
||||
- **FAIL:** при FAIL задача продвигается дальше / не откатывается.
|
||||
|
||||
### AC-12 — task_desc несёт дословную причину (ORCH-046-паттерн)
|
||||
- **PASS:** `task_desc` для перезапущенного developer содержит конкретику находок
|
||||
(какие секреты/CVE), а не только ссылку на артефакт.
|
||||
- **FAIL:** developer получает только ссылку без сути.
|
||||
|
||||
### AC-13 — Cap retry и эскалация
|
||||
- **PASS:** после `MAX_DEVELOPER_RETRIES` (3) безуспешных фиксов — `set_issue_blocked` +
|
||||
Telegram-алерт; бесконечного отскока нет.
|
||||
- **FAIL:** откат зацикливается без cap/эскалации.
|
||||
|
||||
---
|
||||
|
||||
## E. Условный раскат и устойчивость (FR-5/FR-6, BR-8/BR-9)
|
||||
|
||||
### AC-14 — Не-self репозиторий = no-op pass
|
||||
- **PASS:** для repo, не входящего в scope и не self-hosting → гейт возвращает
|
||||
`(True, "security-gate N/A for <repo>")` мгновенно, конвейер такого репо не меняется.
|
||||
- **FAIL:** гейт реально запускается/блокирует чужой репо при пустом scope.
|
||||
|
||||
### AC-15 — Kill-switch отключает гейт
|
||||
- **PASS:** `ORCH_SECURITY_GATE_ENABLED=false` → гейт — no-op pass (`(True, …)`),
|
||||
поведение конвейера 1:1 как до ORCH-022.
|
||||
- **FAIL:** при выключенном флаге гейт всё ещё блокирует.
|
||||
|
||||
### AC-16 — never-raise
|
||||
- **PASS:** искусственный сбой (нет бинаря сканера / таймаут / исключение внутри) →
|
||||
`(False, reason)` без проброса исключения; `advance_stage` не падает, конвейер других
|
||||
задач/проектов не встаёт.
|
||||
- **FAIL:** внутренняя ошибка пробрасывается/вешает движок.
|
||||
|
||||
### AC-17 — Таймаут ограничен
|
||||
- **PASS:** сканирование, превысившее `ORCH_SECURITY_SCAN_TIMEOUT_S`, корректно
|
||||
прерывается → детерминированный вердикт (по политике degrade), без зависания.
|
||||
- **FAIL:** сканер висит без таймаута.
|
||||
|
||||
---
|
||||
|
||||
## F. Инварианты и интеграция (BR-7/BR-12, TRZ §7)
|
||||
|
||||
### AC-18 — STAGE_TRANSITIONS/QG_CHECKS консистентны
|
||||
- **PASS:** при варианте «под-гейт ребра» `STAGE_TRANSITIONS` не изменён; новый чек
|
||||
зарегистрирован в `QG_CHECKS`; `_run_qg` корректно его диспетчеризует. Все
|
||||
существующие тесты гейтов/стадий зелёные.
|
||||
- **FAIL:** сломан реестр/переходы/существующие тесты.
|
||||
|
||||
### AC-19 — Гейт не деплоит/не рестартит прод
|
||||
- **PASS:** код гейта не вызывает деплой-хук/рестарт прод-контейнера; только
|
||||
чтение/сканирование.
|
||||
- **FAIL:** гейт инициирует рестарт/деплой.
|
||||
|
||||
### AC-20 — Документация обновлена в том же PR (BR-12)
|
||||
- **PASS:** обновлены `CLAUDE.md` (артефакт 17-…), `docs/architecture/README.md`
|
||||
(таблица гейтов + реестр QG + раздел ORCH-022), `CHANGELOG.md`, `.env.example`
|
||||
(`ORCH_SECURITY_*`); заведён ADR `06-adr/ADR-001-*`.
|
||||
- **FAIL:** функционал есть, документация/ADR не обновлены → reviewer обязан
|
||||
REQUEST_CHANGES (CLAUDE.md §6).
|
||||
|
||||
### AC-21 — End-to-end на тестовой задаче
|
||||
- **PASS:** прогон на self-hosting-репо: грязная ветка (секрет/CVE) → откат на
|
||||
`development`; после фикса чистая ветка → гейт зелёный → конвейер идёт дальше; прод не
|
||||
затронут в процессе.
|
||||
- **FAIL:** любой шаг E2E не воспроизводится.
|
||||
126
docs/work-items/ORCH-022/04-test-plan.yaml
Normal file
126
docs/work-items/ORCH-022/04-test-plan.yaml
Normal file
@@ -0,0 +1,126 @@
|
||||
work_item: ORCH-022
|
||||
title: "Security-гейт: secret-scanning + dependency audit перед мержем"
|
||||
notes: >
|
||||
План тестов для security-гейта. Чистая логика выносится в leaf-модуль
|
||||
src/security_gate.py (never-raise) — основной предмет unit-тестов (по образцу
|
||||
tests для merge_gate / image_freshness / post_deploy / staging_verdict).
|
||||
Интеграция врезки в advance_stage и условный раскат — integration-тесты.
|
||||
Имена модулей тестов финализирует разработчик/архитектор по факту реализации.
|
||||
|
||||
tests:
|
||||
# --- Secret-scanning (FR-1 / AC-1..AC-3) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "Подсаженный тестовый секрет в diff -> вердикт FAIL, secrets_found>=1, причина называет находку."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "Чистая ветка без секретов -> вердикт PASS, secrets_found=0."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Совпадение из аллоулиста (плейсхолдер .env.example / фикстура) НЕ даёт FAIL."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- Dependency audit + пороги (FR-2 / AC-4..AC-7) ---
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "CVE уровня HIGH/CRITICAL при пороге HIGH -> вклад в FAIL, deps_blocking>=1."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "Только MEDIUM/LOW уязвимости -> PASS, deps_warning>=1, находки в теле артефакта."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "Конфиг порога: severity=CRITICAL делает HIGH-CVE warning; severity=HIGH делает её блоком."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "Недоступный CVE-фид -> детерминированный degrade по политике ADR (дефолт fail-open + warning), без исключения и без ложного FAIL."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- Вердикт / парсер frontmatter (FR-3 / AC-8..AC-10) ---
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Вердикт читается ТОЛЬКО из YAML-frontmatter; проза PASS/FAIL в теле не влияет; negative-токен авторитетен."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "Нет frontmatter / битый YAML / нет поля security_status -> (False, reason) (fail-closed на чтении)."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "Артефакт 17-security-report.md создаётся с валидным frontmatter (security_status, secrets_found, deps_blocking, deps_warning) и телом-списком."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- never-raise / таймаут / условность (FR-5/FR-6 / AC-14..AC-17) ---
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Отсутствие бинаря сканера / внутреннее исключение -> (False, reason), исключение не пробрасывается (never-raise)."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Превышение ORCH_SECURITY_SCAN_TIMEOUT_S -> корректное прерывание и детерминированный вердикт, без зависания."
|
||||
module: tests/test_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "check_security_gate: не-self репо при пустом scope -> (True, 'security-gate N/A for <repo>') мгновенно."
|
||||
module: tests/test_qg_security.py
|
||||
expected: PASS
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "check_security_gate: ORCH_SECURITY_GATE_ENABLED=false -> no-op pass (True)."
|
||||
module: tests/test_qg_security.py
|
||||
expected: PASS
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "Новый чек зарегистрирован в QG_CHECKS и корректно диспетчеризуется _run_qg."
|
||||
module: tests/test_qg_security.py
|
||||
expected: PASS
|
||||
|
||||
# --- Откат / retry в stage_engine (FR-4 / AC-11..AC-13) ---
|
||||
- id: TC-16
|
||||
type: integration
|
||||
description: "security_status FAIL -> advance_stage откатывает на development, enqueue developer, Plane-коммент + notify_qg_failure."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "task_desc перезапущенного developer содержит дословную причину находок (ORCH-046-паттерн), не только ссылку."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "После MAX_DEVELOPER_RETRIES (3) -> set_issue_blocked + Telegram-алерт; бесконечного отскока нет."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
- id: TC-19
|
||||
type: integration
|
||||
description: "security_status PASS -> advance_stage продвигает конвейер штатно (без отката, без шумных нотификаций)."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
|
||||
# --- Инварианты / интеграция (BR-7/BR-12 / AC-18..AC-19) ---
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "При варианте 'под-гейт ребра' STAGE_TRANSITIONS не изменён; существующие тесты стадий/гейтов остаются зелёными."
|
||||
module: tests/test_stages.py
|
||||
expected: PASS
|
||||
- id: TC-21
|
||||
type: integration
|
||||
description: "Гейт не вызывает деплой-хук/рестарт прод-контейнера (self-hosting safety)."
|
||||
module: tests/test_stage_engine_security_gate.py
|
||||
expected: PASS
|
||||
235
docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md
Normal file
235
docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# ADR-001: Security-гейт — secret-scanning + dependency audit перед мержем
|
||||
|
||||
- **Статус:** Accepted (proposed → принято архитектором ORCH-022)
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-022
|
||||
- **Связанный global ADR:** `docs/architecture/adr/adr-0012-security-gate.md`
|
||||
- **Источники:** `01-brd.md` (BR-1..BR-14), `02-trz.md` (FR-1..FR-7, §4 варианты, §7 инварианты),
|
||||
`03-acceptance-criteria.md` (AC-1..AC-21).
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
Оркестратор автономен: `developer`-агент пишет код без человека-фильтра. Перед слиянием
|
||||
ветки задачи в `main` нет автоматической проверки на утёкший секрет (ключ/токен/пароль/
|
||||
приватный ключ) и на уязвимую зависимость (известная CVE). Для self-hosting это особенно
|
||||
опасно: один общий прод-инстанс обслуживает все проекты с общей БД — секрет или CVE,
|
||||
просочившийся через одну задачу, попадает в прод всех проектов (CLAUDE.md §self-hosting, §8).
|
||||
|
||||
Конвейер уже содержит линию детерминированных страховок на ребре `deploy-staging → deploy`
|
||||
(непосредственно перед фактическим мержем PR в `main`, который делает `deployer` в начале
|
||||
стадии `deploy`):
|
||||
|
||||
- **merge-gate** (ORCH-043, `check_branch_mergeable`) — догон `main` + re-test + сериализация;
|
||||
- **image-freshness** (ORCH-058, `check_staging_image_fresh`) — провенанс staging-образа.
|
||||
|
||||
Оба построены по одному паттерну: **leaf-модуль чистой логики (never-raise) + тонкая обёртка
|
||||
в `QG_CHECKS` + врезка-обработчик `_handle_*` в `advance_stage`**, с условным раскатом
|
||||
(`*_enabled` + `*_repos`, реально только для self-hosting при пустом scope) и откатом на
|
||||
`development` с developer-retry (cap `MAX_DEVELOPER_RETRIES = 3`).
|
||||
|
||||
Открытые вопросы BRD §8 / TRZ §4, требующие решения архитектора:
|
||||
1. Размещение гейта в пайплайне (review / merge-edge / CI-job).
|
||||
2. Где запускается сканер (CI-job через `check_ci_green` / отдельный QG-чек).
|
||||
3. Degrade при недоступном CVE-фиде (fail-open / fail-closed).
|
||||
4. Выбор инструментов (gitleaks/trufflehog; pip-audit/trivy).
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
### Р-1. Размещение — Вариант M (под-гейт ребра `deploy-staging → deploy`), ПЕРВЫМ среди edge-под-гейтов
|
||||
|
||||
Security-гейт реализуется как **детерминированный под-гейт того же ребра**
|
||||
`deploy-staging → deploy`, что merge-gate и image-freshness, и исполняется **ПЕРВЫМ** —
|
||||
**ДО** merge-gate. `STAGE_TRANSITIONS` **не меняется** (триггер — то же событие «staging-
|
||||
deployer завершился»; инвариант TRZ §7.1).
|
||||
|
||||
Порядок врезок в `advance_stage` (блок `current_stage == "deploy-staging"`):
|
||||
|
||||
```
|
||||
check_staging_status (PASS, существующий QG стадии)
|
||||
→ security-gate (НОВЫЙ, _handle_security_gate) ← первым
|
||||
→ merge-gate (_handle_merge_gate)
|
||||
→ image-freshness (_handle_image_freshness)
|
||||
→ Phase A (self-deploy approve)
|
||||
```
|
||||
|
||||
**Почему merge-edge, а не review (Вариант R):**
|
||||
- BRD-требование «перед слиянием в `main`» удовлетворяют оба, но на review-стадии diff
|
||||
может разойтись с тем, что реально вольётся в `main` (параллельная задача двигает `main`
|
||||
вперёд между review и merge). Merge-edge — последняя точка перед фактическим мержем.
|
||||
- Переиспользуется готовая машинерия отката/retry/нотификаций edge-под-гейтов
|
||||
(минимальный blast-radius, инвариант TRZ §7).
|
||||
|
||||
**Почему ПЕРВЫМ (до merge-gate), а не после image-freshness:**
|
||||
- **Дёшево фейлить.** merge-gate (rebase + re-test, минуты) и image-freshness (docker
|
||||
rebuild, до 1200с) — дорогие. Нет смысла гонять их на ветке с секретом/CVE.
|
||||
- **Корректность для секретов.** Секрет живёт в собственных коммитах ветки;
|
||||
rebase онто `main` его не добавляет и не убирает → скан диапазона `origin/main..HEAD`
|
||||
до rebase ловит ровно те коммиты, что попадут в `main`.
|
||||
- **Анти-петля для зависимостей.** Аудит ветки **до** rebase оценивает то, что вносит
|
||||
ИМЕННО эта задача (её `requirements.txt`/diff), а не уязвимость, которую притащил в
|
||||
ветку обновившийся `main`. Аудит после rebase «обвинял» бы задачу в чужой (main'овой)
|
||||
CVE → ложный откат `→ development` → петля (прецедент ORCH-061). Скан до rebase этого
|
||||
избегает.
|
||||
- **Проще, чем image-freshness.** Гейт исполняется ДО захвата merge-lease → при FAIL
|
||||
**lease освобождать не нужно** (в отличие от `_handle_image_freshness`). Чистый откат.
|
||||
|
||||
**Почему не CI-job (Вариант C):** пороги severity, warning-vs-block, аллоулист и
|
||||
машиночитаемый артефакт-вердикт плохо выражаются одним статусом коммита Gitea; путь
|
||||
коуплится с CI-раннером. Отклонено для v1; оставлено как точка расширения (BR-14).
|
||||
|
||||
### Р-2. Инструменты
|
||||
|
||||
- **Secret-scanning — `gitleaks`.** Полностью **offline** (без сетевого фида → гарантия
|
||||
«секрет всегда блокирует» не зависит от сети, BR-2), один статический бинарь,
|
||||
детерминированный, конфиг + аллоулист в репо (`.gitleaks.toml`, BR-13), поддержка
|
||||
`--log-opts="origin/main..HEAD"` (скан диапазона), JSON-отчёт, exit-code контракт
|
||||
(0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента). Бинарь устанавливается в
|
||||
`Dockerfile` (Go-бинарь, не pip-пакет) — см. `07-infra-requirements.md`.
|
||||
- **Dependency audit — `pip-audit`.** Python-native (v1-стек — сам оркестратор, Python),
|
||||
читает `requirements.txt`, источник advisory — OSV/PyPI, JSON-выход, ставится через
|
||||
`requirements.txt`. trivy/trufflehog отклонены как тяжелее/контейнер-ориентированные для
|
||||
v1-цели «Python-only» (A3).
|
||||
|
||||
Конкретные инструменты — деталь реализации; контракт гейта (вход: repo/branch/wi,
|
||||
выход: `(bool, reason)` + артефакт) от них не зависит, заменяемы за leaf-модулем.
|
||||
|
||||
### Р-3. Degrade при недоступном CVE-фиде — **fail-open + громкий warning** (дефолт)
|
||||
|
||||
`pip-audit` требует сети (OSV/PyPI advisory DB). Недоступность фида **по умолчанию**:
|
||||
- **fail-open**: dep-audit не даёт FAIL по причине недоступности фида (иначе — ложные
|
||||
откаты `→ development` → петля при сетевых проблемах прод-инстанса, прецедент ORCH-061);
|
||||
- **громко**: в артефакте `deps_audit_degraded: true`, лог `logger.warning`, Telegram-алерт.
|
||||
- **Секреты не деградируют:** gitleaks offline → гарантия BR-2 безусловна даже при
|
||||
отсутствии сети. Деградирует ТОЛЬКО dep-audit.
|
||||
- **Конфигурируемо:** флаг `security_dep_audit_fail_closed` (дефолт `false`) позволяет
|
||||
Owner'у переключить на fail-closed (недоступность фида → FAIL) без редеплоя кода.
|
||||
|
||||
Это разделяет две гарантии: «нет секрета в прод» — **безусловная**; «нет известной CVE» —
|
||||
**best-effort при доступности фида**. Закреплено в acceptance (AC-7).
|
||||
|
||||
### Р-4. Пороги классификации (A4, BR-10)
|
||||
|
||||
- **Секреты:** любой подтверждённый (не из аллоулиста) секрет → **вклад в FAIL** (всегда
|
||||
блок; флаг `security_secrets_block`, дефолт `true`).
|
||||
- **Зависимости:** severity ≥ `security_dep_block_severity` (дефолт `HIGH`) → **вклад в
|
||||
FAIL** (`deps_blocking`); ниже порога (`MEDIUM`/`LOW`) → **warning** (`deps_warning`,
|
||||
не блокирует, фиксируется в теле).
|
||||
- **Severity = UNKNOWN** (OSV/advisory без CVSS — частый случай pip-audit): трактуется как
|
||||
**ниже порога → warning**, никогда не авто-блок (анти-петля). Логируется.
|
||||
|
||||
### Р-5. Артефакт и вердикт (FR-3, BR-6, канон проекта)
|
||||
|
||||
- Новый артефакт **`17-security-report.md`** (следующий свободный номер; финализировано).
|
||||
- YAML-frontmatter:
|
||||
```
|
||||
---
|
||||
security_status: PASS # PASS | FAIL
|
||||
secrets_found: 0
|
||||
deps_blocking: 0
|
||||
deps_warning: 2
|
||||
deps_audit_degraded: false
|
||||
---
|
||||
```
|
||||
Тело — человекочитаемый список находок (секреты: файл/правило/маскированное совпадение;
|
||||
CVE: пакет/версия/идентификатор/severity).
|
||||
- **Единый источник истины:** гейт вычисляет находки → пишет артефакт → **читает вердикт
|
||||
обратно через `parse_security_status(content)`** (frontmatter-парсер по образцу
|
||||
`_parse_deploy_status`/`_parse_staging_status`) → возвращает этот вердикт. Так возвращаемый
|
||||
`(bool, reason)` гарантированно == frontmatter артефакта (канон «машинный вердикт — только
|
||||
из YAML-frontmatter, никогда из прозы», AC-8). Negative-токен (`FAIL`) авторитетен.
|
||||
- Битый/отсутствующий frontmatter / нет поля `security_status` → `(False, reason)` —
|
||||
fail-closed на чтении вердикта (AC-9).
|
||||
|
||||
### Р-6. Поведение красного гейта (FR-4, BR-5)
|
||||
|
||||
`security_status: FAIL` → врезка `_handle_security_gate` (по образцу
|
||||
`_handle_image_freshness`, но БЕЗ работы с lease — гейт до его захвата):
|
||||
- `update_task_stage(development)` + `enqueue_job("developer", …)`;
|
||||
- retry-счётчик — **существующий** `_developer_retry_count` (общий с merge/freshness;
|
||||
без новой колонки, TRZ §6); cap `MAX_DEVELOPER_RETRIES = 3` → при исчерпании
|
||||
`set_issue_blocked` + Telegram;
|
||||
- `task_desc` несёт **дословную причину** (какие секреты/файлы, какие пакеты/CVE/severity)
|
||||
по образцу ORCH-046 — не только ссылку на артефакт (AC-12);
|
||||
- `notify_qg_failure` + Plane-коммент (наблюдаемость BR-11).
|
||||
|
||||
PASS → `return False` из обработчика → `advance_stage` идёт к merge-gate (тишина, без шума).
|
||||
|
||||
### Р-7. Условный раскат и устойчивость (FR-5/FR-6)
|
||||
|
||||
- `check_security_gate(repo, work_item_id, branch)` в `QG_CHECKS`; обёртка делегирует в
|
||||
`src/security_gate.py` (ленивый импорт во избежание цикла — по образцу
|
||||
`_check_staging_image_fresh`).
|
||||
- Условность: `security_gate_enabled=False` → `(True, "security-gate disabled")`;
|
||||
`security_gate_repos` (CSV) пусто → реально только `is_self_hosting_repo` → прочие репо
|
||||
`(True, "security-gate N/A for <repo>")` (AC-14/AC-15).
|
||||
- **never-raise** (двойной guard как `check_branch_mergeable`): любая ошибка (нет бинаря,
|
||||
таймаут, исключение) → `(False, reason)`, исключение не уходит в `advance_stage` (AC-16).
|
||||
- Таймаут сканирования `security_scan_timeout_s` (дефолт 300) на каждый внешний вызов
|
||||
(`subprocess … timeout=`) — превышение → детерминированный degrade-вердикт (AC-17).
|
||||
|
||||
### Р-8. Self-hosting safety (инвариант TRZ §7.5, AC-19)
|
||||
|
||||
Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает
|
||||
деплой-хук, не рестартит и не трогает прод-контейнер (8500/8501).
|
||||
|
||||
---
|
||||
|
||||
## Точки касания (для developer; reviewer проверяет полноту — AC-20)
|
||||
|
||||
| Модуль | Изменение |
|
||||
|--------|-----------|
|
||||
| `src/security_gate.py` (**новый leaf**) | `security_gate_applies`, `scan_secrets`, `audit_dependencies`, `classify_severity`, `compute_verdict`, `write_security_report`, `parse_security_status`, `check_security_gate`. never-raise, fail-closed на чтении вердикта. По образцу `image_freshness.py`. |
|
||||
| `src/qg/checks.py` | `check_security_gate` (тонкая обёртка, ленивый импорт) + регистрация в `QG_CHECKS`. |
|
||||
| `src/stage_engine.py` | `_handle_security_gate(...)` + врезка ПЕРВОЙ в блоке `current_stage == "deploy-staging"` (до `_handle_merge_gate`). FAIL → откат на `development`. never-raise. **`STAGE_TRANSITIONS` НЕ меняется.** |
|
||||
| `src/config.py` | `security_gate_enabled` (True), `security_gate_repos` (""), `security_dep_block_severity` ("HIGH"), `security_scan_timeout_s` (300), `security_dep_audit_fail_closed` (False), `security_secrets_block` (True) — с docstring по образцу ORCH-043/058. |
|
||||
| `Dockerfile` | Установка `gitleaks` (release-бинарь). |
|
||||
| `requirements.txt` | `pip-audit`. |
|
||||
| `.gitleaks.toml` (**новый, корень репо**) | Конфиг правил + аллоулист (`.env.example`-плейсхолдеры, тест-фикстуры) — BR-13. |
|
||||
| `.openclaw/agents/developer.md` | (Опц.) краткая инструкция про устранение security-находок при заворотах. |
|
||||
| `tests/` | `test_security_gate.py`, `test_qg_security.py`, `test_stage_engine_security_gate.py` (см. `04-test-plan.yaml`). |
|
||||
| **Документация** | `CLAUDE.md` (артефакт 17-…), `docs/architecture/README.md` (таблица гейтов + реестр QG + раздел), `CHANGELOG.md`, `.env.example` (`ORCH_SECURITY_*`), global `adr-0012`. |
|
||||
|
||||
---
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
- **Вариант R (review-стадия):** раньше/дешевле, но diff может разойтись с тем, что
|
||||
вольётся в `main`; merge-edge уже закрывает «последнюю страховку».
|
||||
- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо
|
||||
выражаются статусом коммита; коуплинг с CI-раннером. → точка расширения BR-14.
|
||||
- **fail-closed dep-audit по умолчанию:** ложные откаты при сетевых сбоях → петля. →
|
||||
только опционально через флаг.
|
||||
- **Аудит после rebase (как анкер image-freshness):** обвиняет задачу в CVE из `main` →
|
||||
петля. → скан ветки ДО merge-gate.
|
||||
- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как
|
||||
отклонено в ORCH-043). → под-гейт ребра.
|
||||
- **Новая колонка retry в БД:** не нужна — переиспользуем `_developer_retry_count`.
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы.** Структурно невозможно тихо влить секрет (безусловно) или известную CVE
|
||||
(best-effort) в `main`/прод автономной системы. Самоприменение CLAUDE.md §8. Минимальный
|
||||
blast-radius: `STAGE_TRANSITIONS`/схема БД не меняются, переиспользован готовый паттерн.
|
||||
|
||||
**Минусы / плата.** Ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`).
|
||||
Добавлены внешние инструменты (gitleaks-бинарь в образ, pip-audit в зависимости). Время
|
||||
сканирования добавляется к каждому прогону (ограничено таймаутом). Dep-audit best-effort
|
||||
при сетевых сбоях (осознанный компромисс против петли). v1 — Python-only (A3); мульти-стек
|
||||
и SAST — follow-up WI (BR-14).
|
||||
|
||||
**Раскат.** Сквозное изменение конвейера (новый QG + новый edge-под-гейт) → лейбл
|
||||
`arch:major-change`. Прод-деплой ORCH-022 — строго через staging-гейт (8501), без рестарта
|
||||
прод-контейнера в рамках задачи (self-hosting safety).
|
||||
|
||||
## Связи
|
||||
|
||||
adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness —
|
||||
условность/never-raise/fail-closed), adr-0003 (`is_self_hosting_repo` — образец условности),
|
||||
adr-0009/ORCH-061 (анти-петля ложных FAIL), ORCH-046 (дословный reason в `task_desc`),
|
||||
ORCH-9/15 (мульти-стек — будущая зависимость).
|
||||
56
docs/work-items/ORCH-022/07-infra-requirements.md
Normal file
56
docs/work-items/ORCH-022/07-infra-requirements.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 07 — Инфраструктурные требования: Security-гейт (ORCH-022)
|
||||
|
||||
См. `06-adr/ADR-001-security-gate.md` (Р-2, Р-3, Р-8). Топология не меняется (один сервер
|
||||
mva154, Docker Compose). Новые требования — только инструменты сканирования и сетевой доступ
|
||||
к CVE-фиду.
|
||||
|
||||
## I-1. Бинарь `gitleaks` в образе
|
||||
- **Что:** статический Go-бинарь `gitleaks` (secret-scanning), устанавливается в `Dockerfile`
|
||||
(НЕ pip-пакет). Зафиксировать версию (pinned release) для детерминизма.
|
||||
- **Почему в образе, а не на хосте:** гейт исполняется внутри контейнера оркестратора
|
||||
(`advance_stage`); сканируется per-task worktree, смонтированный в контейнер.
|
||||
- **Оффлайн:** gitleaks не требует сети (правила локальны) → гарантия «секрет всегда
|
||||
блокирует» (BR-2) не зависит от доступности интернета.
|
||||
- **Контракт exit-кодов:** 0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента
|
||||
(≥2 → never-raise degrade-вердикт гейта).
|
||||
|
||||
## I-2. `pip-audit` в зависимостях
|
||||
- **Что:** Python-пакет `pip-audit` (dependency audit), добавляется в `requirements.txt`
|
||||
(pinned-версия).
|
||||
- **Источник advisory:** OSV / PyPI advisory DB — **требует сетевого доступа** (исходящий
|
||||
HTTPS к OSV/PyPI).
|
||||
- **Цель v1:** аудит `requirements.txt` корня репо (Python-стек, A3). Мульти-стек — follow-up.
|
||||
|
||||
## I-3. Сетевой доступ к CVE-фиду (degrade-политика)
|
||||
- **Требование:** исходящий HTTPS из прод-контейнера к OSV/PyPI advisory.
|
||||
- **При недоступности (Р-3):** **fail-open + громкий warning** по умолчанию — dep-audit не
|
||||
краснит гейт из-за сетевого сбоя (анти-петля ORCH-061); фиксируется
|
||||
`deps_audit_degraded: true` + Telegram + лог. Флаг `security_dep_audit_fail_closed`
|
||||
(дефолт `false`) — для перевода в строгий режим без редеплоя кода.
|
||||
- **Секреты не зависят от сети** (I-1) — критическая гарантия безусловна.
|
||||
|
||||
## I-4. Конфиг-файлы в репозитории (версионируемые, BR-13)
|
||||
- `.gitleaks.toml` (корень репо): правила + аллоулист заведомо-безопасных совпадений
|
||||
(плейсхолдеры `.env.example`, тест-фикстуры). Версионируется, ревьюится как код.
|
||||
|
||||
## I-5. Env-флаги (`.env.example` + хост `.env`/`.env.staging`)
|
||||
| Переменная | Дефолт | Назначение |
|
||||
|------------|--------|-----------|
|
||||
| `ORCH_SECURITY_GATE_ENABLED` | `true` | глобальный kill-switch |
|
||||
| `ORCH_SECURITY_GATE_REPOS` | `` (пусто) | CSV scope; пусто → только self-hosting |
|
||||
| `ORCH_SECURITY_DEP_BLOCK_SEVERITY` | `HIGH` | порог блокировки зависимостей |
|
||||
| `ORCH_SECURITY_SCAN_TIMEOUT_S` | `300` | таймаут каждого внешнего вызова сканера |
|
||||
| `ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED` | `false` | строгий режим при недоступном фиде |
|
||||
| `ORCH_SECURITY_SECRETS_BLOCK` | `true` | секреты блокируют (всегда по дефолту) |
|
||||
|
||||
Секреты-значения в гит НЕ коммитятся (CLAUDE.md §8) — только дефолты в `.env.example`.
|
||||
|
||||
## I-6. Ресурсы и тайминги
|
||||
- Время сканирования добавляется к каждому прогону задачи на ребре `deploy-staging → deploy`,
|
||||
ограничено `ORCH_SECURITY_SCAN_TIMEOUT_S` (по образцу `merge_retest_timeout_s`).
|
||||
- Гейт исполняется ДО merge-gate/image-freshness (дёшево фейлить до дорогих rebase/rebuild).
|
||||
|
||||
## I-7. Self-hosting safety (инвариант)
|
||||
Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает
|
||||
деплой-хук, не рестартит/не трогает прод-контейнер (8500/8501). Прод-деплой ORCH-022 — строго
|
||||
через staging-гейт (8501).
|
||||
26
docs/work-items/ORCH-022/08-data-requirements.md
Normal file
26
docs/work-items/ORCH-022/08-data-requirements.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 08 — Требования к схеме БД: Security-гейт (ORCH-022)
|
||||
|
||||
## Решение: схема БД НЕ меняется
|
||||
|
||||
Миграций нет. Обоснование (соответствует TRZ §6 и паттерну edge-под-гейтов ORCH-043/058):
|
||||
|
||||
1. **Вердикт гейта — артефакт-файл** `17-security-report.md` (YAML-frontmatter), как
|
||||
`14-deploy-log.md` / `15-staging-log.md`. Не хранится в БД.
|
||||
2. **Состояние/идемпотентность** — детерминированная пересборка вердикта при каждом тике
|
||||
(гейт чистый, без долгоживущего состояния между прогонами); sentinel-файлы НЕ требуются
|
||||
(в отличие от deploy-state/post-deploy-state — там асинхронный self-restart).
|
||||
3. **Retry-счётчик** — переиспользуется существующий `_developer_retry_count(task_id)`
|
||||
(подсчёт по `jobs`/`agent_runs`), общий с merge-gate/image-freshness. **Новой колонки
|
||||
`security_retry` НЕ вводим** (TRZ §6: предпочесть подсчёт по `jobs`/`agent_runs`). Это
|
||||
корректно: security-FAIL, как merge/freshness-FAIL, откатывает на `development` и
|
||||
запускает developer — он и есть единица retry; общий cap=3 защищает от петли.
|
||||
|
||||
## Используемые существующие таблицы (без изменений)
|
||||
- `tasks` — стадия задачи (`update_task_stage` при откате на `development`).
|
||||
- `jobs` — enqueue `developer` при FAIL; основа `_developer_retry_count`.
|
||||
- `agent_runs` — usage/duration; основа подсчёта retry.
|
||||
|
||||
## Что НЕ делаем
|
||||
- Не добавляем таблицу findings/CVE-журнала (история находок — в артефактах per-task; петля
|
||||
уроков ORCH-8 читает артефакт).
|
||||
- Не добавляем колонок в `tasks`/`jobs`.
|
||||
16
docs/work-items/ORCH-022/10-tech-risks.md
Normal file
16
docs/work-items/ORCH-022/10-tech-risks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 10 — Технические риски: Security-гейт (ORCH-022)
|
||||
|
||||
| ID | Риск | Вероятность / Влияние | Митигация (заложена в ADR-001) |
|
||||
|----|------|----------------------|-------------------------------|
|
||||
| R-1 | **Ложные срабатывания → петля отката** `→ development` (прецедент ORCH-061 staging-loop). | Средн. / Выс. | Аллоулист `.gitleaks.toml` (BR-13); cap `MAX_DEVELOPER_RETRIES=3` → эскалация (`set_issue_blocked`+Telegram); конфигурируемый порог severity; kill-switch; UNKNOWN-severity → warning, не блок. |
|
||||
| R-2 | **Недоступность CVE-фида** даёт ложный красный/исключение. | Средн. / Выс. | fail-open + громкий warning по умолчанию (Р-3); `deps_audit_degraded:true`; флаг `security_dep_audit_fail_closed` для строгого режима. Секреты offline → не затронуты. |
|
||||
| R-3 | **Скан вешает worker-слот** (зависший gitleaks/pip-audit) → стоит конвейер всех проектов (общий инстанс, `max_concurrency`). | Низк. / Выс. | `security_scan_timeout_s` (300) на каждый внешний вызов; never-raise degrade-вердикт; гейт ПЕРВЫМ на ребре (фейлит до дорогих rebase/rebuild). |
|
||||
| R-4 | **Исключение гейта роняет `advance_stage`** → встаёт движок. | Низк. / Выс. | Двойной never-raise guard (внешний+внутренний) как `check_branch_mergeable`; AC-16/TC-11. |
|
||||
| R-5 | **Скан после rebase обвиняет задачу в CVE из `main`** → петля. | — (устранён дизайном) | Гейт исполняется ДО merge-gate (скан ветки до rebase); Р-1. |
|
||||
| R-6 | **Отсутствие бинаря `gitleaks` в образе** (забыт в Dockerfile) → гейт всегда degrade. | Низк. / Средн. | Установка в Dockerfile (I-1), pinned-версия; TC-11 (нет бинаря → `(False,reason)`, never-raise); проверяется на staging (8501) до прода. |
|
||||
| R-7 | **pip-audit без severity (UNKNOWN)** → либо ложный блок, либо пропуск. | Средн. / Средн. | UNKNOWN → warning (не блок), логируется; осознанный анти-петля компромисс; ужесточение — follow-up. |
|
||||
| R-8 | **Self-hosting: гейт трогает прод** (рестарт/деплой). | — (запрещено дизайном) | Гейт только читает/сканирует; AC-19/TC-21; прод-деплой ORCH-022 — через staging-гейт. |
|
||||
| R-9 | **Drift вердикта vs артефакта** (возврат ≠ frontmatter). | Низк. / Средн. | Единый источник: гейт пишет артефакт → читает обратно через `parse_security_status` → возвращает (Р-5); AC-8. |
|
||||
| R-10 | **Регресс существующих гейтов/стадий** (сломан `QG_CHECKS`/`STAGE_TRANSITIONS`). | Низк. / Выс. | `STAGE_TRANSITIONS` не меняется; новый чек — аддитивно в реестр; полный прогон `tests/` (TC-20); staging-гейт перед прод. |
|
||||
| R-11 | **v1 Python-only** — секреты/CVE в не-Python стеке (JS/Android) не ловятся. | — (вне scope v1, A3) | Условность scope; точка расширения мульти-стек/SAST (BR-14); зависимость ORCH-9/15 зафиксирована. |
|
||||
| R-12 | **Стоимость времени** на каждом прогоне задачи. | Низк. / Низк. | Таймаут; гейт первым (ранний выход); только self-hosting по умолчанию. |
|
||||
74
docs/work-items/ORCH-022/12-review.md
Normal file
74
docs/work-items/ORCH-022/12-review.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-022
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-022
|
||||
|
||||
## Summary
|
||||
Security-гейт (secret-scanning `gitleaks` + dependency audit `pip-audit`) реализован как
|
||||
детерминированный под-гейт ребра `deploy-staging → deploy`, исполняемый ПЕРВЫМ среди
|
||||
edge-под-гейтов — в точности по ADR-001 (Вариант M) и эталонному паттерну соседей
|
||||
(merge-gate ORCH-043 / image-freshness ORCH-058): leaf-модуль `src/security_gate.py`
|
||||
(never-raise) + тонкая обёртка `check_security_gate` в `QG_CHECKS` (lazy-import, нет цикла)
|
||||
+ врезка `_handle_security_gate` ПЕРВОЙ в блоке `current_stage == "deploy-staging"`.
|
||||
`STAGE_TRANSITIONS` и схема БД не тронуты. Все 772 теста зелёные (25 из них —
|
||||
security-специфичные: `test_security_gate.py`, `test_qg_security.py`,
|
||||
`test_stage_engine_security_gate.py`). Документация обновлена полностью и в этом же PR.
|
||||
|
||||
### Соответствие ТЗ (02-trz)
|
||||
- FR-1 secret-scan offline `origin/main..HEAD`, любой секрет вне аллоулиста → FAIL ✓
|
||||
- FR-2 dep-audit по severity (`HIGH` дефолт), MEDIUM/LOW/UNKNOWN → warning ✓
|
||||
- FR-3 машинный вердикт ТОЛЬКО из frontmatter `17-security-report.md`, negative-токен
|
||||
авторитетен, write→read-back (единый источник истины) ✓
|
||||
- FR-4 FAIL → откат на `development` + developer-retry (cap 3) + `task_desc` с дословными
|
||||
находками (ORCH-046) ✓
|
||||
- FR-5 условность `security_gate_enabled` / `security_gate_repos` (пусто → self-hosting) ✓
|
||||
- FR-6 never-raise + таймаут `security_scan_timeout_s` ✓
|
||||
- FR-7 наблюдаемость (Telegram при degraded/FAIL, лог при PASS) ✓
|
||||
- §6 без миграций БД, §7 инварианты соблюдены (STAGE_TRANSITIONS/QG_CHECKS консистентны,
|
||||
gate не деплоит/не рестартит прод) ✓
|
||||
|
||||
### Соответствие ADR (06-adr/ADR-001 + global adr-0012)
|
||||
Р-1 (размещение ПЕРВЫМ, до merge-gate, до захвата merge-lease → lease не освобождается),
|
||||
Р-2 (gitleaks pinned Go-бинарь в Dockerfile, pip-audit в requirements), Р-3 (fail-open
|
||||
degrade + флаг `security_dep_audit_fail_closed`), Р-4 (пороги, UNKNOWN→warning), Р-5
|
||||
(артефакт + read-back), Р-6 (откат/cap/эскалация), Р-7 (lazy-import, double-guard
|
||||
never-raise), Р-8 (self-hosting safety) — все реализованы как описано.
|
||||
|
||||
### Критерии приёмки (03)
|
||||
AC-1..AC-21 покрыты тестами TC-01..TC-21 (incl. rollback TC-16, verbatim task_desc TC-17,
|
||||
cap+blocked TC-18, PASS-advance TC-19, no-deploy-on-FAIL TC-21). AC-20 (документация) —
|
||||
подтверждён ниже.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice-to-have
|
||||
- Глобальный `docs/architecture/adr/adr-0012-security-gate.md` помечен `Статус: proposed`,
|
||||
тогда как per-WI `06-adr/ADR-001` — `Accepted`. Косметическая рассинхронизация статуса,
|
||||
на функциональность/гейты не влияет.
|
||||
|
||||
## Документация
|
||||
Обновлена в том же PR (AC-20, CLAUDE.md §6 соблюдён):
|
||||
- `CLAUDE.md` — раздел «Артефакты задачи» (добавлен `17-security-report.md`) + строка о
|
||||
машинных вердиктах (`security_status:`).
|
||||
- `docs/architecture/README.md` — реестр `QG_CHECKS` (`check_security_gate (ORCH-022)`),
|
||||
новый раздел «Security-гейт …», статусная сноска внизу.
|
||||
- `docs/architecture/adr/adr-0012-security-gate.md` — новый global ADR (+ per-WI ADR-001).
|
||||
- `CHANGELOG.md` — подробная запись в `[Unreleased] / Added`.
|
||||
- `.env.example` — все шесть `ORCH_SECURITY_*` с комментариями.
|
||||
- `Dockerfile` (pinned gitleaks), `requirements.txt` (pip-audit), `.gitleaks.toml` (корень,
|
||||
правила + аллоулист) — инфраструктура версионирована.
|
||||
|
||||
Статус: документация = golden source — синхронна с кодом. Замечаний нет.
|
||||
76
docs/work-items/ORCH-022/13-test-report.md
Normal file
76
docs/work-items/ORCH-022/13-test-report.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-022
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-022
|
||||
|
||||
Security-гейт: secret-scanning (gitleaks) + dependency audit (pip-audit) как под-гейт
|
||||
ребра `deploy-staging → deploy`.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Дата: 2026-06-07
|
||||
- Ветка: `feature/ORCH-022-security-secret-scanning`
|
||||
- Review verdict: APPROVED (`12-review.md`)
|
||||
|
||||
## Smoke test API (prod 8500, self-hosting — не трогаем контейнер)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
|
||||
| `GET /status` | OK (active task ORCH-022 в stage=testing виден) |
|
||||
| `GET /queue` | OK (counts/resilience/reconcile/reaper/post_deploy присутствуют) |
|
||||
|
||||
## Результаты (привязка к 04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Тест | Результат |
|
||||
|-------|----------|------|-----------|
|
||||
| TC-01 | Секрет в diff → FAIL, secrets_found>=1, причина называет находку | test_security_gate.py::test_tc01_secret_in_diff_fails | PASS |
|
||||
| TC-02 | Чистая ветка → PASS, secrets_found=0 | test_tc02_clean_branch_passes | PASS |
|
||||
| TC-03 | Аллоулист подавляет заведомо-безопасное | test_tc03_allowlisted_match_does_not_fail | PASS |
|
||||
| TC-04 | HIGH/CRITICAL CVE при пороге HIGH → FAIL, deps_blocking>=1 | test_tc04_high_cve_at_high_threshold_blocks | PASS |
|
||||
| TC-05 | Только MEDIUM/LOW → PASS, deps_warning>=1 | test_tc05_only_medium_low_warns_passes | PASS |
|
||||
| TC-06 | Конфиг порога severity влияет на классификацию | test_tc06_threshold_config_changes_classification | PASS |
|
||||
| TC-07 | Недоступный фид → детерминированный degrade (fail-open default / fail-closed strict) | test_tc07_degraded_feed_failopen_default_failclosed_strict | PASS |
|
||||
| TC-08 | Вердикт ТОЛЬКО из frontmatter; negative-токен авторитетен | test_tc08_verdict_only_from_frontmatter | PASS |
|
||||
| TC-09 | Нет/битый frontmatter → (False, reason) fail-closed | test_tc09_missing_or_broken_frontmatter_failclosed | PASS |
|
||||
| TC-10 | Артефакт 17-security-report.md с валидным frontmatter + телом | test_tc10_artifact_has_valid_frontmatter_and_body | PASS |
|
||||
| TC-11 | Нет бинаря / исключение → (False, reason), never-raise | test_tc11_missing_binary_failclosed_never_raises | PASS |
|
||||
| TC-12 | Таймаут → детерминированный fail-closed, без зависания | test_tc12_timeout_is_deterministic_failclosed | PASS |
|
||||
| TC-13 | Не-self репо при пустом scope → (True, N/A) мгновенно | test_qg_security.py::test_tc13_non_self_repo_empty_scope_is_na | PASS |
|
||||
| TC-14 | ORCH_SECURITY_GATE_ENABLED=false → no-op pass | test_tc14_disabled_is_noop_pass | PASS |
|
||||
| TC-15 | Зарегистрирован в QG_CHECKS и диспетчеризуется _run_qg | test_tc15_registered_in_qg_checks / test_tc15_dispatched_by_run_qg | PASS |
|
||||
| TC-16 | FAIL → откат на development, enqueue developer, notify_qg_failure | test_stage_engine_security_gate.py::test_tc16_fail_rolls_back_and_enqueues_developer | PASS |
|
||||
| TC-17 | task_desc несёт дословную причину (ORCH-046) | test_tc17_task_desc_has_verbatim_findings | PASS |
|
||||
| TC-18 | После MAX_DEVELOPER_RETRIES (3) → set_issue_blocked + Telegram | test_tc18_retry_cap_blocks_and_alerts | PASS |
|
||||
| TC-19 | PASS → штатное продвижение конвейера | test_tc19_pass_advances_normally | PASS |
|
||||
| TC-20 | STAGE_TRANSITIONS не изменён; тесты стадий зелёные | tests/test_stages.py (полный прогон) | PASS |
|
||||
| TC-21 | Гейт не вызывает деплой-хук/рестарт прод (self-hosting safety) | test_tc21_fail_never_triggers_deploy | PASS |
|
||||
|
||||
Все 21 TC покрыты и зелёные. Соответствие критериям приёмки (03-acceptance-criteria):
|
||||
AC-1..AC-21 закрыты соответствующими TC (AC-N ↔ TC-N для N=1..21; AC-20 «документация»
|
||||
подтверждён в review 12-review.md).
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
### Security-специфичные тесты (25 шт.)
|
||||
```
|
||||
tests/test_security_gate.py ............... (15)
|
||||
tests/test_qg_security.py ...... (6)
|
||||
tests/test_stage_engine_security_gate.py ..... (5)
|
||||
======================== 25 passed, 1 warning in 0.49s =========================
|
||||
```
|
||||
|
||||
### Полный регресс
|
||||
```
|
||||
======================= 772 passed, 1 warning in 14.70s ========================
|
||||
```
|
||||
(1 warning — PydanticDeprecatedSince20 в src/config.py, не связан с ORCH-022,
|
||||
существовал до задачи.)
|
||||
|
||||
## Итог
|
||||
**PASS** — полный регресс 772/772 зелёный, 25 security-тестов покрывают все 21 TC
|
||||
плана и AC-1..AC-21, smoke-тесты API прод-инстанса OK. Прод-контейнер в процессе
|
||||
тестирования не затронут (тесты офлайн/изолированы). Задача готова к стадии deploy-staging.
|
||||
12
docs/work-items/ORCH-022/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-022/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-022
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
30
docs/work-items/ORCH-022/15-staging-log.md
Normal file
30
docs/work-items/ORCH-022/15-staging-log.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T18:02:27+00:00
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed via canonical run (ORCH-048, ADR-001):
|
||||
|
||||
```
|
||||
docker exec orchestrator-staging \
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
**Result: 8/10 checks PASS — exit code 0 (advance).**
|
||||
|
||||
All REAL (pipeline) checks green: A1, A2, A3 (SMOKE), B4, B5, B6 (ACCESS), C7, C8 (E2E).
|
||||
|
||||
Two sandbox-infra-only checks failed and were waived per ORCH-061
|
||||
(`staging_infra_tolerance_enabled=True`) — these depend on SANDBOX bot accounts
|
||||
being members of the SANDBOX Plane project, not on the pipeline:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
Cleanup ran (Plane SANDBOX test issue deleted, HTTP 204). Exit code 0 → `staging_status: SUCCESS`.
|
||||
14
docs/work-items/ORCH-022/16-post-deploy-log.md
Normal file
14
docs/work-items/ORCH-022/16-post-deploy-log.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY
|
||||
action_taken: NONE
|
||||
work_item: ORCH-022
|
||||
window_s: 900
|
||||
checks_total: 30
|
||||
checks_failed: 0
|
||||
---
|
||||
|
||||
# Post-deploy log — ORCH-021 post-deploy monitor
|
||||
|
||||
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
|
||||
|
||||
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.
|
||||
7
docs/work-items/ORCH-059/00-business-request.md
Normal file
7
docs/work-items/ORCH-059/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Approve деплоя через статус Confirm Deploy (вместо перегруженного Approved)
|
||||
|
||||
Work Item ID: ORCH-059
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
115
docs/work-items/ORCH-059/01-brd.md
Normal file
115
docs/work-items/ORCH-059/01-brd.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# 01 — BRD: Approve прод-деплоя через выделенный статус «Confirm Deploy»
|
||||
|
||||
Work Item: **ORCH-059**
|
||||
Repo: `orchestrator`
|
||||
Stage: analysis
|
||||
Тип: enhancement / risk-reduction (self-hosting)
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
В ORCH-036 («исполняемый самодеплой стадии `deploy`») прод-деплой self-hosting
|
||||
инстанса (контейнер `orchestrator`, порт 8500) запускается **Фазой B**: человек
|
||||
переводит issue в Plane-статус **`Approved`**, webhook
|
||||
`work_item.updated` → `handle_issue_updated` → `handle_verdict(approved=True)`
|
||||
→ `_try_advance_stage` → `advance_stage(finished_agent=None)`, и в
|
||||
`stage_engine.advance_stage` срабатывает блок
|
||||
`current_stage == "deploy" and finished_agent is None` →
|
||||
`_handle_self_deploy_phase_b` → detached host-деплой прода.
|
||||
|
||||
**Перегрузка статуса.** Тот же самый Plane-статус `Approved` (UUID
|
||||
`a519a341-…`) используется как **человеческий гейт одобрения BRD** на ранней
|
||||
стадии `analysis` (`check_analysis_approved`: analysis → architecture) и в общем
|
||||
verdict-роутинге `handle_verdict`. Один и тот же визуальный «Approved» на доске
|
||||
означает две принципиально разные вещи:
|
||||
|
||||
- на `analysis` — «BRD/ТЗ/AC приняты, продолжай конвейер» (дёшево, обратимо);
|
||||
- на `deploy` — «**ВЫКАТИ В ПРОД** инструмент, который прямо сейчас обслуживает
|
||||
все проекты из одного инстанса с общей БД» (дорого, групповой риск, см.
|
||||
раздел Self-hosting в `CLAUDE.md`).
|
||||
|
||||
### Последствия (Pain)
|
||||
- **Двусмысленность семантики.** Один статус — два смысла; оператор не видит из
|
||||
названия, что клик на `deploy` запускает реальный прод-рестарт.
|
||||
- **Риск случайного клика.** Привычный жест «Approved» (которым оператор
|
||||
штатно одобряет BRD десятки раз) на стадии `deploy` молча триггерит
|
||||
прод-деплой. Цена ошибки — незапланированный рестарт прод-инстанса,
|
||||
встающий конвейер всех проектов.
|
||||
- **Несоответствие ожиданиям ORCH-036.** В scope ORCH-36 заявлялась Telegram
|
||||
inline-кнопка подтверждения; в коде её **нет** — developer реализовал approve
|
||||
исключительно через Plane-статус. Отдельного «осознанного» жеста подтверждения
|
||||
деплоя в системе сейчас не существует.
|
||||
|
||||
## 2. Решение Owner
|
||||
|
||||
Ввести **отдельный Plane-статус `Confirm Deploy`** в проекте ORCH, который
|
||||
триггерит **ТОЛЬКО** Фазу B self-deploy на стадии `deploy`. Статус `Approved`
|
||||
перестаёт запускать прод-деплой и сохраняет единственный смысл — человеческое
|
||||
одобрение на гейтах конвейера (прежде всего BRD на `analysis`).
|
||||
|
||||
Минимальная правка: `handle_verdict` в `src/webhooks/plane.py` + регистрация
|
||||
нового состояния в проекте ORCH (Plane + резолвер состояний).
|
||||
|
||||
## 3. Бизнес-цели
|
||||
- **BG-1.** Убрать двусмысленность: жест «запустить прод-деплой» отделён от жеста
|
||||
«одобрить артефакт».
|
||||
- **BG-2.** Снизить риск случайного прод-деплоя: запуск прода требует явного,
|
||||
редко используемого статуса `Confirm Deploy`, а не привычного `Approved`.
|
||||
- **BG-3.** Не сломать работающий self-hosting конвейер при доработке самого
|
||||
инструмента (нулевая регрессия `analysis`-гейта и не-self репозиториев).
|
||||
|
||||
## 4. Объём (Scope)
|
||||
|
||||
### В объёме
|
||||
- Новый логический статус `confirm_deploy` («Confirm Deploy») в резолвере
|
||||
состояний Plane (`src/plane_sync.py`).
|
||||
- Маршрутизация нового статуса в `src/webhooks/plane.py`
|
||||
(`handle_issue_updated` / `handle_verdict`) на путь Фазы B прод-деплоя.
|
||||
- Прекращение триггера Фазы B по статусу `Approved` на стадии `deploy`.
|
||||
- Обновление текста CTA Фазы A (Plane-комментарий + Telegram в
|
||||
`stage_engine._handle_self_deploy_phase_a`): инструктировать оператора
|
||||
переводить задачу в `Confirm Deploy`, а не в `Approved`.
|
||||
- Конфигурация Plane: создание статуса «Confirm Deploy» в проекте ORCH
|
||||
(предусловие эксплуатации — фиксируется в TRZ/AC как требование среды).
|
||||
- Обновление документации (`CLAUDE.md`, `docs/architecture/README.md` секция
|
||||
ORCH-036, `CHANGELOG.md`) и ADR per-work-item.
|
||||
|
||||
### Вне объёма
|
||||
- Telegram inline-кнопки подтверждения деплоя (отдельная задача; здесь не
|
||||
реализуем — управление по-прежнему статусом Plane).
|
||||
- Полностью автоматический approve деплоя (ORCH-54).
|
||||
- Изменение Фаз A/C, exit-кодов хука, merge-gate, `check_deploy_status`,
|
||||
схемы БД, реестров `STAGE_TRANSITIONS` / `QG_CHECKS`.
|
||||
- Поведение прод-деплоя для не-self репозиториев (остаётся прежним).
|
||||
- Post-deploy наблюдение (ORCH-021) — не затрагивается.
|
||||
|
||||
## 5. Заинтересованные стороны
|
||||
- **Owner/оператор** — переводит задачи по статусам; главный выгодоприобретатель
|
||||
снижения риска.
|
||||
- **Self-hosting конвейер** — все проекты на общем инстансе; косвенно зависят от
|
||||
безопасности прод-деплоя орка.
|
||||
|
||||
## 6. Допущения
|
||||
- A-1. Plane позволяет добавить кастомный статус «Confirm Deploy» в проект ORCH;
|
||||
его UUID резолвится через `get_project_states` (API `/states/`).
|
||||
- A-2. Статус `Confirm Deploy` нужен только проекту ORCH (self-hosting). Прочие
|
||||
проекты прод-деплой через Plane-approve не используют
|
||||
(`self_deploy_applies` → только `orchestrator`).
|
||||
- A-3. Оператор переводит задачу в `Confirm Deploy` только когда она реально
|
||||
находится на стадии `deploy` (approval-pending после Фазы A).
|
||||
|
||||
## 7. Риски (детально — 10-tech-risks.md, ведёт архитектор)
|
||||
- R-1. Новый логический ключ `confirm_deploy` отсутствует в fallback
|
||||
`_DEFAULT_STATES` и в проектах без этого статуса → обращение к ключу должно
|
||||
быть безопасным (fail-closed: нет статуса → нет деплоя, не падение).
|
||||
- R-2. Регрессия: `Approved` на `deploy` после правки не должен НИ
|
||||
запускать деплой, НИ вызывать ложный откат/advance.
|
||||
- R-3. Самоправка прода: правка не должна потребовать ручного рестарта прод-
|
||||
контейнера вне штатной стадии deploy-staging → deploy.
|
||||
|
||||
## 8. Definition of Done (бизнес-уровень)
|
||||
- Перевод задачи стадии `deploy` в `Confirm Deploy` запускает прод-деплой
|
||||
(Фаза B) ровно так же, как раньше делал `Approved`.
|
||||
- Перевод задачи стадии `deploy` в `Approved` прод-деплой НЕ запускает.
|
||||
- `Approved` на `analysis` (и прочих человеческих гейтах) работает без изменений.
|
||||
- CTA Фазы A просит `Confirm Deploy`.
|
||||
- Документация и ADR обновлены в том же PR.
|
||||
103
docs/work-items/ORCH-059/02-trz.md
Normal file
103
docs/work-items/ORCH-059/02-trz.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 02 — ТЗ: выделенный статус «Confirm Deploy» как триггер прод-деплоя
|
||||
|
||||
Work Item: **ORCH-059** · Repo: `orchestrator` · Stage: analysis
|
||||
|
||||
> ТЗ описывает **что** должно измениться и **поведенческий контракт**. Конкретный
|
||||
> дизайн (сигнатуры, способ проброса признака «confirm-deploy» из webhook в
|
||||
> `stage_engine`, sentinel-обработка) — за архитектором (ADR per-work-item).
|
||||
> Точки касания ниже заданы бизнес-запросом Owner и текущей реализацией ORCH-036.
|
||||
|
||||
## 1. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль в задаче |
|
||||
|--------|---------------|
|
||||
| `src/plane_sync.py` | Резолвер состояний Plane. Добавить логический ключ `confirm_deploy` ↔ имя статуса «Confirm Deploy»; обеспечить безопасный доступ при отсутствии статуса (fallback/неполный конфиг). |
|
||||
| `src/webhooks/plane.py` | `handle_issue_updated` — маршрутизация нового статуса; `handle_verdict` — отделить «подтверждение деплоя» от обычного approve; снять триггер Фазы B со статуса `Approved` на `deploy`. |
|
||||
| `src/stage_engine.py` | Блок Фазы B (`current_stage == "deploy" and finished_agent is None`) должен срабатывать ТОЛЬКО по сигналу confirm-deploy, не по обычному Approved. Обновить CTA-текст Фазы A (`_handle_self_deploy_phase_a`). |
|
||||
| `src/config.py` | (опционально, на усмотрение архитектора) флаг/имя статуса, если потребуется конфигурируемость. По умолчанию — не требуется. |
|
||||
|
||||
## 2. Поведенческий контракт (требования)
|
||||
|
||||
### TRZ-1. Регистрация статуса «Confirm Deploy»
|
||||
Резолвер состояний (`get_project_states`) обязан возвращать UUID статуса
|
||||
«Confirm Deploy» под логическим ключом `confirm_deploy` для проекта ORCH.
|
||||
Маппинг имени `"Confirm Deploy" → "confirm_deploy"` добавляется в
|
||||
`_PLANE_NAME_TO_KEY`. Для проектов/сред, где статус отсутствует (enduro,
|
||||
fallback `_DEFAULT_STATES`, недоступный API), ключ может отсутствовать —
|
||||
обращение к нему должно быть **fail-closed**: «нет статуса → ветка confirm-deploy
|
||||
не активируется», без `KeyError`/исключения.
|
||||
|
||||
### TRZ-2. Триггер прод-деплоя по «Confirm Deploy»
|
||||
Когда задача находится на стадии `deploy` и issue переводится в статус
|
||||
`Confirm Deploy`, система обязана инициировать **Фазу B** прод-деплоя
|
||||
(эквивалент текущего `_handle_self_deploy_phase_b`: idempotency-guard `initiated`,
|
||||
`self_deploy.initiate_deploy`, постановка `deploy-finalizer`, комментарии/Telegram).
|
||||
Поведение, идемпотентность и Фаза C — **без изменений** относительно ORCH-036;
|
||||
меняется только **что именно является триггером**.
|
||||
|
||||
### TRZ-3. `Approved` больше не запускает прод-деплой
|
||||
Перевод задачи стадии `deploy` в статус `Approved` **не должен** инициировать
|
||||
Фазу B. Он не должен также вызывать ложный откат (БАГ-8) или ложный advance
|
||||
по `check_deploy_status` (вердикта ещё нет). Допустимое поведение — **no-op с
|
||||
логированием** (issue остаётся на `deploy`/approval-pending). Конкретный способ
|
||||
(игнор на уровне webhook-роутинга или на уровне `stage_engine`) — за архитектором.
|
||||
|
||||
### TRZ-4. Сохранность гейта `Approved` на остальных стадиях
|
||||
Статус `Approved` обязан продолжать работать как человеческий гейт:
|
||||
- `analysis` → `architecture` (`check_analysis_approved`, approved-via-status);
|
||||
- любой иной человеческий approve-advance, существующий сегодня.
|
||||
Регрессия `handle_verdict(approved=True)` для НЕ-`deploy` стадий недопустима.
|
||||
|
||||
### TRZ-5. CTA Фазы A
|
||||
Текст запроса approve в `_handle_self_deploy_phase_a` (Plane-комментарий + Telegram)
|
||||
обязан инструктировать оператора переводить задачу в статус **`Confirm Deploy`**
|
||||
(а не `Approved`) для запуска прод-деплоя.
|
||||
|
||||
### TRZ-6. Условность (как ORCH-35/36)
|
||||
Ветка confirm-deploy реальна только для self-hosting
|
||||
(`self_deploy.self_deploy_applies(repo)` → `orchestrator`). Для прочих репо —
|
||||
прежнее поведение (синхронный деплой агентом), статус `Confirm Deploy` не
|
||||
требуется и не влияет.
|
||||
|
||||
## 3. Изменения API
|
||||
Изменений HTTP-эндпоинтов **нет**. Канал — существующий `POST /webhook/plane`
|
||||
(событие `work_item.updated`). Внешнее изменение: в проекте ORCH появляется
|
||||
дополнительный статус доски «Confirm Deploy» (Plane-конфигурация, не код-API).
|
||||
|
||||
## 4. Изменения схемы БД
|
||||
**Нет.** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, таблицы `tasks`/`jobs`/
|
||||
`agent_runs`/`events` — без изменений. Статусы — на стороне Plane; restart-safe
|
||||
состояние деплоя — существующие sentinel-файлы ORCH-036 (без миграций).
|
||||
|
||||
## 5. Требования к новым QG checks
|
||||
**Нет.** Новый Quality Gate не вводится. `check_deploy_status` /
|
||||
`_parse_deploy_status` и контракт exit-кодов хука (0/1/2) — без изменений.
|
||||
|
||||
## 6. Конфигурация среды (предусловие эксплуатации)
|
||||
- В проекте ORCH в Plane создаётся статус доски **«Confirm Deploy»** (точное имя,
|
||||
чувствительно к регистру — должно совпасть с ключом `_PLANE_NAME_TO_KEY`).
|
||||
- Размещение статуса на доске — рядом со стадией deploy/approval-pending
|
||||
(рекомендация эксплуатации, не код).
|
||||
- Кэш состояний (`get_project_states` / `reload_project_states`): после создания
|
||||
статуса может потребоваться сброс кэша или рестарт по штатной стадии deploy.
|
||||
|
||||
## 7. Артефакты, создаваемые/обновляемые по pipeline
|
||||
- `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` — решение
|
||||
(как отличается триггер; где разрезается перегрузка `Approved`; fail-closed
|
||||
при отсутствии статуса) — **ведёт архитектор**.
|
||||
- `CLAUDE.md` — упоминание выделенного статуса approve прод-деплоя (раздел
|
||||
self-hosting / артефакты).
|
||||
- `docs/architecture/README.md` — секция ORCH-036: уточнить, что Фаза B
|
||||
триггерится статусом `Confirm Deploy`, а не `Approved`.
|
||||
- `CHANGELOG.md` — запись ORCH-059.
|
||||
- `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md` —
|
||||
штатно по стадиям конвейера.
|
||||
|
||||
## 8. Совместимость и инварианты
|
||||
- Не меняются: `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`,
|
||||
БАГ-8 (FAILED → откат на development), merge-gate, exit-коды хука, Фазы A/C,
|
||||
схема БД, post-deploy (ORCH-021).
|
||||
- Self-hosting safety: правка НЕ требует внепланового рестарта прод-контейнера;
|
||||
выкат — через штатный deploy-staging (8501) → deploy.
|
||||
- Never-crash: отсутствие статуса `Confirm Deploy` в резолвере не приводит к
|
||||
исключению в webhook-пути.
|
||||
76
docs/work-items/ORCH-059/03-acceptance-criteria.md
Normal file
76
docs/work-items/ORCH-059/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 03 — Критерии приёмки: ORCH-059
|
||||
|
||||
Repo: `orchestrator` · Stage: analysis
|
||||
Каждый критерий — однозначный PASS/FAIL. Проверка: unit/integration (см.
|
||||
`04-test-plan.yaml`) + ручная верификация для инфра-предусловий.
|
||||
|
||||
## AC-1 — Статус «Confirm Deploy» резолвится
|
||||
**Given** проект ORCH со статусом доски «Confirm Deploy»
|
||||
**When** вызывается резолвер состояний для проекта ORCH
|
||||
**Then** возвращается логический ключ `confirm_deploy` с непустым UUID,
|
||||
а маппинг `"Confirm Deploy" → "confirm_deploy"` присутствует в `_PLANE_NAME_TO_KEY`.
|
||||
**FAIL:** ключ отсутствует или указывает на UUID статуса `Approved`.
|
||||
|
||||
## AC-2 — «Confirm Deploy» на стадии `deploy` запускает Фазу B
|
||||
**Given** задача self-hosting (`orchestrator`) на стадии `deploy`,
|
||||
`deploy_require_manual_approve=true`, маркер `initiated` отсутствует
|
||||
**When** приходит `work_item.updated` со статусом `Confirm Deploy`
|
||||
**Then** инициируется Фаза B: вызывается `self_deploy.initiate_deploy`,
|
||||
ставится job `deploy-finalizer`, пишется маркер `initiated`.
|
||||
**FAIL:** прод-деплой не инициирован, либо finalizer не поставлен.
|
||||
|
||||
## AC-3 — «Approved» на стадии `deploy` НЕ запускает прод-деплой
|
||||
**Given** та же задача на стадии `deploy`
|
||||
**When** приходит `work_item.updated` со статусом `Approved`
|
||||
**Then** `self_deploy.initiate_deploy` **НЕ** вызывается; Фаза B не стартует;
|
||||
задача не откатывается (БАГ-8 не срабатывает) и не «доходит» по
|
||||
`check_deploy_status` (вердикта нет); событие залогировано как no-op.
|
||||
**FAIL:** вызван `initiate_deploy`, либо произошёл откат/ложный advance.
|
||||
|
||||
## AC-4 — «Approved» на `analysis` работает без регрессии
|
||||
**Given** задача на стадии `analysis` (BRD готов, approval-pending)
|
||||
**When** issue переводится в `Approved`
|
||||
**Then** срабатывает approved-via-status и задача продвигается
|
||||
`analysis → architecture` (как до правки).
|
||||
**FAIL:** approve на analysis перестал продвигать конвейер.
|
||||
|
||||
## AC-5 — Идемпотентность Фазы B по «Confirm Deploy»
|
||||
**Given** задача на `deploy`, маркер `initiated` уже существует
|
||||
**When** повторно приходит статус `Confirm Deploy` (двойной клик / дубль webhook)
|
||||
**Then** повторного `initiate_deploy` не происходит (no-op,
|
||||
`self-deploy-already-initiated`).
|
||||
**FAIL:** прод-деплой запускается повторно.
|
||||
|
||||
## AC-6 — CTA Фазы A просит «Confirm Deploy»
|
||||
**Given** Фаза A (`deploy-staging → deploy`, approval-pending)
|
||||
**When** формируются Plane-комментарий и Telegram-уведомление запроса approve
|
||||
**Then** текст инструктирует перевести задачу в статус **`Confirm Deploy`**
|
||||
(а не «Approved») для запуска прод-деплоя.
|
||||
**FAIL:** CTA по-прежнему упоминает только «Approved».
|
||||
|
||||
## AC-7 — Fail-closed при отсутствии статуса
|
||||
**Given** среда без статуса «Confirm Deploy» (enduro / fallback `_DEFAULT_STATES`
|
||||
/ недоступный Plane API)
|
||||
**When** обрабатывается `work_item.updated`
|
||||
**Then** webhook-путь не выбрасывает исключение; ветка confirm-deploy не
|
||||
активируется (прод-деплой не запускается «вслепую»).
|
||||
**FAIL:** `KeyError`/исключение в обработчике, либо ложный запуск Фазы B.
|
||||
|
||||
## AC-8 — Условность для не-self репозиториев
|
||||
**Given** не-self репозиторий (`self_deploy_applies(repo) == False`)
|
||||
**When** приходит любой verdict-статус на стадии `deploy`
|
||||
**Then** поведение прод-деплоя не меняется относительно текущего (синхронный
|
||||
деплой агентом); статус `Confirm Deploy` не требуется.
|
||||
**FAIL:** изменилось поведение деплоя не-self проекта.
|
||||
|
||||
## AC-9 — Инварианты не нарушены
|
||||
**Then** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_deploy_status`/
|
||||
`_parse_deploy_status`, контракт exit-кодов хука (0/1/2), Фазы A/C, merge-gate,
|
||||
схема БД — без изменений; `pytest tests/ -q` зелёный.
|
||||
**FAIL:** изменён любой из перечисленных контрактов или красные тесты.
|
||||
|
||||
## AC-10 — Документация обновлена (golden source)
|
||||
**Then** в том же PR обновлены `CLAUDE.md`, секция ORCH-036 в
|
||||
`docs/architecture/README.md`, `CHANGELOG.md`; заведён
|
||||
`06-adr/ADR-001-confirm-deploy-status.md`.
|
||||
**FAIL:** функционал изменён, документация — нет (Reviewer → REQUEST_CHANGES).
|
||||
109
docs/work-items/ORCH-059/04-test-plan.yaml
Normal file
109
docs/work-items/ORCH-059/04-test-plan.yaml
Normal file
@@ -0,0 +1,109 @@
|
||||
work_item: ORCH-059
|
||||
title: Approve прод-деплоя через выделенный статус «Confirm Deploy»
|
||||
repo: orchestrator
|
||||
stage: analysis
|
||||
|
||||
# Контракт-тесты: триггер прод-деплоя смещается с перегруженного `Approved`
|
||||
# на выделенный статус `Confirm Deploy`. Деплой и сетевые вызовы мокаются.
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "_PLANE_NAME_TO_KEY содержит маппинг 'Confirm Deploy' -> 'confirm_deploy'"
|
||||
module: tests/test_plane_states.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >-
|
||||
get_project_states для проекта ORCH (мок API со статусом 'Confirm Deploy')
|
||||
возвращает непустой UUID под ключом 'confirm_deploy', отличный от 'approved'
|
||||
module: tests/test_plane_states.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >-
|
||||
Fail-closed: при отсутствии статуса 'Confirm Deploy' (fallback _DEFAULT_STATES /
|
||||
недоступный API) доступ к ключу confirm_deploy не выбрасывает исключение
|
||||
и не активирует ветку confirm-deploy
|
||||
module: tests/test_plane_states.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >-
|
||||
handle_issue_updated: статус 'Confirm Deploy' на задаче стадии deploy
|
||||
маршрутизируется на путь Фазы B (а не на обычный approve/advance)
|
||||
module: tests/test_plane_confirm_deploy.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: >-
|
||||
handle_verdict/Approved на стадии deploy НЕ вызывает self_deploy.initiate_deploy
|
||||
(initiate_deploy замокан и не должен быть вызван)
|
||||
module: tests/test_plane_confirm_deploy.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: >-
|
||||
Approved на стадии analysis по-прежнему продвигает analysis -> architecture
|
||||
(approved-via-status, регрессия гейта check_analysis_approved)
|
||||
module: tests/test_plane_confirm_deploy.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: >-
|
||||
stage_engine: блок Фазы B (current_stage==deploy, finished_agent is None)
|
||||
инициирует deploy ТОЛЬКО по сигналу confirm-deploy; Approved-сигнал -> no-op
|
||||
module: tests/test_stage_engine_phase_b.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: >-
|
||||
Идемпотентность: при существующем маркере 'initiated' повторный
|
||||
Confirm Deploy не вызывает initiate_deploy (self-deploy-already-initiated)
|
||||
module: tests/test_stage_engine_phase_b.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: >-
|
||||
CTA Фазы A (_handle_self_deploy_phase_a): текст Plane-комментария и Telegram
|
||||
содержат 'Confirm Deploy' и не предлагают 'Approved' как триггер деплоя
|
||||
module: tests/test_stage_engine_phase_a_cta.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: integration
|
||||
description: >-
|
||||
E2E (мок Plane API + self_deploy): задача на deploy -> webhook Confirm Deploy
|
||||
-> initiate_deploy вызван, deploy-finalizer поставлен, маркер initiated записан
|
||||
module: tests/test_confirm_deploy_integration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: integration
|
||||
description: >-
|
||||
E2E: задача на deploy -> webhook Approved -> прод-деплой НЕ инициирован,
|
||||
задача остаётся на deploy (нет отката, нет advance в done)
|
||||
module: tests/test_confirm_deploy_integration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: integration
|
||||
description: >-
|
||||
Условность: для не-self репозитория verdict-статусы на deploy не меняют
|
||||
поведение деплоя (self_deploy_applies == False)
|
||||
module: tests/test_confirm_deploy_integration.py
|
||||
expected: PASS
|
||||
|
||||
regression:
|
||||
- id: RG-01
|
||||
type: integration
|
||||
description: "pytest tests/ -q зелёный; STAGE_TRANSITIONS и QG_CHECKS без изменений"
|
||||
module: tests/
|
||||
expected: PASS
|
||||
156
docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md
Normal file
156
docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# ADR-001 (ORCH-059): Выделенный статус «Confirm Deploy» как триггер прод-деплоя
|
||||
|
||||
## Статус
|
||||
Accepted (design) — реализация в ветке `feature/ORCH-059-approve-confirm-deploy-approve`.
|
||||
|
||||
## Контекст
|
||||
ORCH-036 (исполняемый самодеплой стадии `deploy`) запускает прод-деплой
|
||||
self-hosting инстанса **Фазой B**: человек переводит issue в Plane-статус
|
||||
`Approved` → webhook `work_item.updated` → `handle_issue_updated` →
|
||||
`handle_verdict(approved=True)` → `_try_advance_stage` →
|
||||
`advance_stage(finished_agent=None)`; в `stage_engine.advance_stage` блок
|
||||
`current_stage == "deploy" and finished_agent is None` →
|
||||
`_handle_self_deploy_phase_b` → detached host-деплой прода (8500).
|
||||
|
||||
Тот же UUID `Approved` (`a519a341-…`, `_DEFAULT_STATES["approved"]`) — это
|
||||
**человеческий гейт одобрения** на стадии `analysis`
|
||||
(`check_analysis_approved`, путь `approved-via-status`) и общий verdict-роутинг
|
||||
в `handle_verdict`. Один визуальный «Approved» на доске значит две принципиально
|
||||
разные вещи: «принять BRD» (дёшево, обратимо) и «**ВЫКАТИТЬ В ПРОД** инструмент,
|
||||
обслуживающий все проекты из одного инстанса с общей БД» (дорого, групповой
|
||||
риск). Привычный жест approve на стадии `deploy` молча триггерит прод-рестарт —
|
||||
цена случайного клика высока (см. self-hosting в `CLAUDE.md`).
|
||||
|
||||
Ограничения, формирующие дизайн (см. `02-trz.md`, `03-acceptance-criteria.md`):
|
||||
1. **Нулевая регрессия** гейта `Approved` на `analysis` и прочих стадиях (TRZ-4).
|
||||
2. **Fail-closed**: среды без статуса (enduro, fallback `_DEFAULT_STATES`,
|
||||
недоступный API) не должны падать и не должны «вслепую» деплоить (TRZ-1, R-1).
|
||||
3. **`Approved` на `deploy` не должен** запускать Фазу B И не должен вызывать
|
||||
ложный откат (БАГ-8) или ложный advance по `check_deploy_status` — вердикта
|
||||
ещё нет (TRZ-3, R-2).
|
||||
4. **Без правки контрактов**: `STAGE_TRANSITIONS`, `QG_CHECKS`,
|
||||
`check_deploy_status`, Фазы A/C, merge-gate, exit-коды хука, схема БД (TRZ-8).
|
||||
5. **Self-hosting safety**: правка — чистая маршрутизация, не требует внепланового
|
||||
рестарта прода; выкат через штатный `deploy-staging` (8501) → `deploy` (R-3).
|
||||
|
||||
## Решение
|
||||
Ввести отдельный логический статус `confirm_deploy` («Confirm Deploy»), который
|
||||
триггерит **ТОЛЬКО** Фазу B на стадии `deploy`. `Approved` теряет смысл «запусти
|
||||
прод-деплой» и остаётся исключительно человеческим гейтом конвейера.
|
||||
|
||||
Четыре точечные правки в трёх модулях:
|
||||
|
||||
### 1. Резолвер состояний — `src/plane_sync.py`
|
||||
- В `_PLANE_NAME_TO_KEY` добавить маппинг `"Confirm Deploy" → "confirm_deploy"`.
|
||||
- В `_DEFAULT_STATES` ключ `confirm_deploy` **НЕ добавлять** (реального UUID для
|
||||
enduro/fallback нет; отсутствие ключа = fail-closed). Для проекта ORCH ключ
|
||||
резолвится `get_project_states` из живого Plane API; для проектов без статуса и
|
||||
на fallback-пути ключ просто отсутствует в результирующем словаре.
|
||||
- Следствие: `get_project_states(orch)["confirm_deploy"]` → реальный UUID;
|
||||
`get_project_states(enduro).get("confirm_deploy")` → `None`.
|
||||
|
||||
### 2. Маршрутизация webhook — `src/webhooks/plane.py`
|
||||
В `handle_issue_updated`, **до** ветки `approved`, добавить fail-closed-ветку:
|
||||
```python
|
||||
confirm_state = proj_states.get("confirm_deploy") # .get -> AC-7/R-1
|
||||
if confirm_state and new_state == confirm_state:
|
||||
await handle_confirm_deploy(data, project_id)
|
||||
elif new_state == proj_states["in_progress"]:
|
||||
...
|
||||
elif new_state == proj_states["approved"]:
|
||||
await handle_verdict(data, project_id, approved=True)
|
||||
```
|
||||
Новый `handle_confirm_deploy(data, project_id)`:
|
||||
- резолвит задачу по `plane_id`;
|
||||
- если `stage != "deploy"` → **no-op с логом** (Confirm Deploy осмыслен только на
|
||||
approval-pending стадии `deploy`; защищает прочие гейты от случайного approve);
|
||||
- иначе → `_try_advance_stage(..., confirm_deploy=True)`.
|
||||
|
||||
`handle_verdict(approved=True)` не меняется — продолжает звать `_try_advance_stage`
|
||||
с `confirm_deploy=False` (дефолт).
|
||||
|
||||
### 3. Сигнал в движок — `src/stage_engine.advance_stage(...)`
|
||||
Добавить keyword-only параметр `confirm_deploy: bool = False` (back-compat: все
|
||||
существующие вызовы из launcher/reconciler/finalizer/webhook передают
|
||||
`finished_agent`, новый kwarg дефолтный). Блок Фазы B переписать так, чтобы он
|
||||
**всегда возвращался рано** для `deploy + finished_agent is None` self-hosting,
|
||||
но деплоил только по сигналу:
|
||||
```python
|
||||
if (current_stage == "deploy" and finished_agent is None
|
||||
and settings.deploy_require_manual_approve
|
||||
and self_deploy.self_deploy_applies(repo)):
|
||||
if confirm_deploy:
|
||||
_handle_self_deploy_phase_b(task_id, repo, work_item_id, branch, result)
|
||||
else:
|
||||
# TRZ-3/R-2: обычный Approved на deploy — no-op; НЕ запускаем
|
||||
# check_deploy_status (вердикта ещё нет -> ложный откат БАГ-8).
|
||||
result.note = "approved-on-deploy-noop"
|
||||
return result
|
||||
```
|
||||
Ключевое: возврат **до** блока Quality Gate в обоих случаях → `check_deploy_status`
|
||||
по `Approved` на `deploy` не исполняется. Фаза C (finalizer,
|
||||
`finished_agent="deployer"`) не затронута — условие требует `finished_agent is
|
||||
None`.
|
||||
|
||||
### 4. CTA Фазы A — `src/stage_engine._handle_self_deploy_phase_a`
|
||||
Текст Plane-комментария и Telegram изменить: вместо «смените статус на Approved»
|
||||
инструктировать перевести задачу в статус **«Confirm Deploy»** для запуска
|
||||
прод-деплоя (TRZ-5/AC-6).
|
||||
|
||||
### Условность (как ORCH-35/36)
|
||||
Вся ветка реальна только для `self_deploy.self_deploy_applies(repo)` →
|
||||
`orchestrator`. Прочие репо — прежний синхронный ssh-деплой агентом; статус
|
||||
`Confirm Deploy` им не нужен и на них не влияет (AC-8).
|
||||
|
||||
## Альтернативы
|
||||
- **A. Telegram inline-кнопка подтверждения** вместо нового статуса — отклонено:
|
||||
кнопочная инфраструктура в коде отсутствует, заявлено вне scope (ORCH-036 п.
|
||||
«inline-кнопка» не реализован); управление остаётся статусом Plane.
|
||||
- **B. Добавить `confirm_deploy` в `_DEFAULT_STATES`** — отклонено: реального UUID
|
||||
«Confirm Deploy» для enduro/fallback нет; пришлось бы подставить фиктивный или
|
||||
дублирующий UUID, что ломает fail-closed (enduro «получил бы» триггер деплоя) и
|
||||
смешивает семантику.
|
||||
- **C. Отдельный публичный entrypoint `stage_engine.initiate_confirm_deploy()`**,
|
||||
минующий `advance_stage` — отклонено: дублирует гарды
|
||||
(`deploy_require_manual_approve`, `self_deploy_applies`, idempotency `initiated`),
|
||||
и всё равно пришлось бы внутри `advance_stage` гасить `Approved`-на-`deploy` в
|
||||
no-op. Параметр-сигнал проще и держит единую точку правды.
|
||||
- **D. Сигнал через sentinel-маркер, записываемый webhook’ом** — отклонено: вызов
|
||||
синхронный в пределах одного `advance_stage`, persistence не нужна; параметр
|
||||
явнее и не плодит файловое состояние.
|
||||
|
||||
## Последствия
|
||||
**Плюсы**
|
||||
- Жест «запустить прод-деплой» отделён от «одобрить артефакт»; случайный approve
|
||||
на доске больше не роняет прод (BG-1, BG-2).
|
||||
- `Approved` на `deploy` детерминированно безопасен: no-op без отката/advance
|
||||
(закрывает R-2).
|
||||
- Fail-closed: нет статуса → нет деплоя, нет исключения (R-1, AC-7).
|
||||
- Минимальный диффузный риск: контракты `STAGE_TRANSITIONS`/`QG_CHECKS`/
|
||||
`check_deploy_status`/Фазы A/C/merge-gate/схема БД не тронуты (AC-9).
|
||||
- Реконсилятор F-1 на `deploy` (finished_agent=None) теперь попадает в no-op-ветку
|
||||
вместо прежнего неявного запуска Фазы B → прод-деплой невозможно инициировать
|
||||
автоматически, только явным человеческим `Confirm Deploy` (усиление safety).
|
||||
|
||||
**Минусы / цена**
|
||||
- Эксплуатационное предусловие: в Plane-проекте ORCH нужно создать статус доски
|
||||
«Confirm Deploy» (точное имя, регистр) и сбросить кэш состояний — см.
|
||||
`07-infra-requirements.md`. До создания статуса прод-деплой через approve не
|
||||
запустится (это и есть желаемое fail-closed-поведение).
|
||||
- Сигнатура `advance_stage` расширена одним kwarg (обратносовместимо).
|
||||
|
||||
**Хэндофф документации (golden source, в том же PR — стадия development).**
|
||||
ADR (этот файл) — артефакт архитектора. Переписать `Approve = Approved` →
|
||||
`Confirm Deploy` в `docs/architecture/README.md` (секция ORCH-036), `CLAUDE.md`
|
||||
(self-hosting/артефакты) и добавить запись в `CHANGELOG.md` обязан developer
|
||||
одновременно с кодом (AC-10), чтобы доки не описывали ещё не существующее
|
||||
поведение. В README на стадии architecture добавлена forward-looking пометка
|
||||
ORCH-059 (design), как принято для незамёрженных доработок.
|
||||
|
||||
## Связанные ADR
|
||||
- `adr-0007-executable-self-deploy.md` (ORCH-036) — задаёт Фазы A/B/C; ORCH-059
|
||||
меняет **только триггер** Фазы B (`Approved` → `Confirm Deploy`) и делает
|
||||
`Approved`-на-`deploy` no-op; Фазы внутренне не меняются.
|
||||
- `adr-0003-staging-gate.md` (ORCH-35) — паттерн условности self-hosting.
|
||||
- `adr-0007-reconciler.md` (ORCH-053) — реконсилятор F-1: поведение на `deploy`
|
||||
становится no-op (см. Последствия).
|
||||
44
docs/work-items/ORCH-059/07-infra-requirements.md
Normal file
44
docs/work-items/ORCH-059/07-infra-requirements.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 07 — Требования к инфраструктуре: ORCH-059
|
||||
|
||||
Work Item: **ORCH-059** · Repo: `orchestrator`
|
||||
Связано: `06-adr/ADR-001-confirm-deploy-status.md`, `02-trz.md` §6.
|
||||
|
||||
> Топология контейнеров/портов/деплоя НЕ меняется (см. `docs/operations/INFRA.md`).
|
||||
> Единственное инфра-требование ORCH-059 — конфигурация Plane-доски проекта ORCH.
|
||||
|
||||
## IR-1. Статус доски «Confirm Deploy» в проекте ORCH (предусловие эксплуатации)
|
||||
- В Plane-проекте **ORCH** создать кастомный статус доски с **точным** именем
|
||||
`Confirm Deploy` (case-sensitive, ровно один пробел) — должно посимвольно
|
||||
совпасть с ключом `_PLANE_NAME_TO_KEY["Confirm Deploy"]`. Несовпадение →
|
||||
fail-closed (деплой не запустится), не краш (R-9).
|
||||
- UUID статуса генерирует Plane; код резолвит его через `get_project_states`
|
||||
(`GET /workspaces/<ws>/projects/<orch>/states/`). Хардкодить UUID не нужно.
|
||||
- **Размещение** на доске — рядом с approval-pending/`deploy` (рекомендация
|
||||
эксплуатации, на поведение кода не влияет).
|
||||
- **Только проект ORCH** (self-hosting). Для enduro и прочих проектов статус НЕ
|
||||
создаётся и НЕ требуется — `self_deploy_applies` истинно лишь для `orchestrator`.
|
||||
|
||||
## IR-2. Сброс кэша состояний после создания статуса
|
||||
`get_project_states` кэширует резолв per-project на время жизни процесса
|
||||
(`_STATES_CACHE`). После создания статуса в Plane закэшированный словарь не
|
||||
содержит `confirm_deploy` (R-5). Применить ОДНО из:
|
||||
- вызвать `reload_project_states(<orch_project_id>)` (или полный сброс), либо
|
||||
- штатно перезапустить прод по конвейеру `deploy-staging → deploy` (рестарт
|
||||
процесса очищает кэш).
|
||||
|
||||
> Внеплановый ручной рестарт прод-контейнера для применения этой задачи **не
|
||||
> требуется** и противопоказан (self-hosting групповой риск). Выкат — только через
|
||||
> штатный staging→deploy.
|
||||
|
||||
## IR-3. Контрольная проверка готовности среды
|
||||
После IR-1+IR-2:
|
||||
1. `get_project_states(<orch>)` содержит `confirm_deploy` с непустым UUID,
|
||||
отличным от `approved` (AC-1, TC-02).
|
||||
2. Перевод тестовой задачи стадии `deploy` (sandbox) в `Confirm Deploy` запускает
|
||||
Фазу B; перевод в `Approved` — нет (AC-2/AC-3).
|
||||
|
||||
## Что НЕ меняется
|
||||
- Порты (8500 prod / 8501 staging), контейнеры, compose-профили, env-карта,
|
||||
деплой-хук, схема БД, sentinel-каталоги ORCH-036 — без изменений.
|
||||
- HTTP-эндпоинты (`POST /webhook/plane` тот же канал, событие
|
||||
`work_item.updated`).
|
||||
25
docs/work-items/ORCH-059/10-tech-risks.md
Normal file
25
docs/work-items/ORCH-059/10-tech-risks.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 10 — Технические риски: ORCH-059
|
||||
|
||||
Work Item: **ORCH-059** · Repo: `orchestrator` · ведёт: архитектор
|
||||
Связано: `06-adr/ADR-001-confirm-deploy-status.md`.
|
||||
|
||||
| ID | Риск | Вероятн. | Влияние | Митигация | Проверка |
|
||||
|----|------|----------|---------|-----------|----------|
|
||||
| R-1 | Ключ `confirm_deploy` отсутствует в `_DEFAULT_STATES` / у проектов без статуса → `KeyError` в webhook-пути | Сред | Выс (краш обработчика) | Доступ ТОЛЬКО через `.get("confirm_deploy")`; `_DEFAULT_STATES` не содержит ключ намеренно; отсутствие → ветка не активируется (fail-closed) | TC-03, AC-7 |
|
||||
| R-2 | `Approved` на `deploy` после правки вызывает `check_deploy_status` (вердикта нет) → ложный откат БАГ-8 / ложный advance | Выс | Выс (петля dev↔deploy, ложный rollback прода) | Блок Фазы B возвращается рано для `deploy + finished_agent is None` self-hosting в ОБОИХ случаях; `Approved` → `note=approved-on-deploy-noop`, QG не запускается | TC-05, TC-07, TC-11, AC-3 |
|
||||
| R-3 | Самоправка прода требует внепланового рестарта прод-контейнера | Низ | Выс (встаёт конвейер всех проектов) | Изменение — чистая маршрутизация в коде; выкат через штатный `deploy-staging` (8501) → `deploy`; sentinel-состояние ORCH-036 не трогаем | AC-9, RG-01 |
|
||||
| R-4 | `Confirm Deploy` прислан на не-`deploy` стадии (оператор ошибся) → срабатывает как обычный approve и продвигает чужой гейт | Низ | Сред | `handle_confirm_deploy` гардит `stage == "deploy"`; иначе no-op с логом | TC-04 (+ ручная верификация) |
|
||||
| R-5 | Кэш `get_project_states` закэширован до создания статуса «Confirm Deploy» → ключ не виден после конфигурации Plane | Сред | Сред (деплой не запускается) | После создания статуса в Plane — `reload_project_states(orch)` или штатный рестарт по стадии `deploy`; зафиксировано в `07-infra-requirements.md` | ручная верификация |
|
||||
| R-6 | Новый kwarg `confirm_deploy` ломает существующие вызовы `advance_stage` (launcher/reconciler/finalizer) | Низ | Выс | keyword-only с дефолтом `False`; все вызовы передают `finished_agent`; не-`deploy`/finished_agent≠None пути не затронуты | RG-01, AC-9 |
|
||||
| R-7 | Регрессия идемпотентности Фазы B (двойной `Confirm Deploy`) | Низ | Сред | Внутренности `_handle_self_deploy_phase_b` (маркер `initiated`) не меняются; меняется только триггер | TC-08, AC-5 |
|
||||
| R-8 | Реконсилятор F-1 на `deploy` (finished_agent=None) меняет поведение | Низ | Низ (улучшение) | Намеренно: раньше неявно мог войти в Фазу B, теперь → no-op. Прод-деплой инициируется только явным `Confirm Deploy`. Документировано в ADR/README | RG-01 |
|
||||
| R-9 | Несовпадение имени статуса в Plane и `_PLANE_NAME_TO_KEY` (регистр/пробел) → ключ не резолвится | Сред | Сред (деплой не запускается, fail-closed) | Точное имя «Confirm Deploy» (case-sensitive) — требование среды в `07-infra-requirements.md`; маппинг ровно этой строкой | TC-01, TC-02 |
|
||||
|
||||
## Сводный вывод
|
||||
Все риски — низкого/среднего остаточного уровня после митигаций. Доминирующий
|
||||
класс — **fail-closed**: любая неполнота конфигурации (нет статуса, протухший кэш,
|
||||
недоступный API) приводит к «деплой не запускается», а не к «деплой запускается
|
||||
вслепую» или к крашу. Контракты конвейера (`STAGE_TRANSITIONS`, `QG_CHECKS`,
|
||||
`check_deploy_status`, Фазы A/C, merge-gate, схема БД) не затрагиваются, поэтому
|
||||
поверхность регрессии ограничена тремя модулями (`plane_sync.py`,
|
||||
`webhooks/plane.py`, `stage_engine.py`).
|
||||
59
docs/work-items/ORCH-059/12-review.md
Normal file
59
docs/work-items/ORCH-059/12-review.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-059
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-059
|
||||
|
||||
## Summary
|
||||
Выделенный Plane-статус «Confirm Deploy» как единственный триггер Фазы B прод-деплоя
|
||||
self-hosting; `Approved` на стадии `deploy` становится детерминированным no-op. Реализация
|
||||
точно соответствует ТЗ (TRZ-1..6), ADR-001 и критериям приёмки (AC-1..10). Четыре точечные
|
||||
правки в трёх модулях (`plane_sync.py`, `webhooks/plane.py`, `stage_engine.py`), без изменения
|
||||
контрактов (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, Фазы A/C, merge-gate, схема
|
||||
БД). Документация обновлена в том же PR. `pytest tests/ -q` — 763 passed.
|
||||
|
||||
## Соответствие ТЗ и ADR
|
||||
- **TRZ-1 / AC-1** — `"Confirm Deploy" → "confirm_deploy"` добавлен в `_PLANE_NAME_TO_KEY`;
|
||||
намеренно отсутствует в `_DEFAULT_STATES` → fail-closed. Покрыто `test_tc01/tc02`.
|
||||
- **TRZ-2 / AC-2** — `handle_confirm_deploy` (гард `stage=="deploy"`) →
|
||||
`_try_advance_stage(..., confirm_deploy=True)` → Фаза B. Покрыто `test_tc04/tc07/tc10`.
|
||||
- **TRZ-3 / AC-3** — `Approved` на `deploy`: ранний возврат ДО Quality Gate с
|
||||
`note="approved-on-deploy-noop"`, без `initiate_deploy`, без ложного отката БАГ-8.
|
||||
Покрыто `test_tc05/tc07_approved_without_confirm_is_noop/tc11`.
|
||||
- **TRZ-4 / AC-4** — `handle_verdict(approved=True)` не тронут; approve на `analysis`
|
||||
продвигает конвейер. Покрыто `test_tc06_approved_on_analysis_still_advances`.
|
||||
- **AC-5** — идемпотентность повторного «Confirm Deploy» (`self-deploy-already-initiated`).
|
||||
Покрыто `test_tc08`, `test_tc06_approved_calls_prod_hook_exactly_once`.
|
||||
- **TRZ-5 / AC-6** — CTA Фазы A (Plane-коммент + Telegram) просит «Confirm Deploy» и явно
|
||||
отмечает, что «Approved» прод-деплой не запускает. Покрыто `test_tc09`.
|
||||
- **TRZ-1 / AC-7** — доступ через `.get("confirm_deploy")`, отсутствие статуса → ветка не
|
||||
активируется, без `KeyError`. Покрыто `test_tc03` (API недоступен / статуса нет на доске).
|
||||
- **TRZ-6 / AC-8** — условность через `self_deploy.self_deploy_applies`; не-self репо без
|
||||
изменений. Покрыто `test_tc12`.
|
||||
- **AC-9** — контракты и схема БД не изменены; 763 теста зелёные.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
## Документация
|
||||
Обновлено в том же PR (AC-10 выполнен):
|
||||
- `CLAUDE.md` — раздел self-hosting: прод-деплой только через «Confirm Deploy», `Approved` = no-op.
|
||||
- `docs/architecture/README.md` — секция ORCH-036 уточнена + добавлена подсекция ORCH-059
|
||||
(статус-триггер «Confirm Deploy»), запись в перечне статусов доработок.
|
||||
- `CHANGELOG.md` — запись ORCH-059 в `[Unreleased] / Added`.
|
||||
- ADR `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` — заведён, отражает
|
||||
реализацию (4 правки, fail-closed, рассмотренные альтернативы).
|
||||
- `07-infra-requirements.md` — эксплуатационное предусловие (создать статус доски + сброс кэша).
|
||||
|
||||
Документация консистентна с кодом; golden-source инвариант соблюдён.
|
||||
71
docs/work-items/ORCH-059/13-test-report.md
Normal file
71
docs/work-items/ORCH-059/13-test-report.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-059
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-059
|
||||
|
||||
Выделенный Plane-статус «Confirm Deploy» как единственный триггер Фазы B прод-деплоя
|
||||
self-hosting; `Approved` на стадии `deploy` — детерминированный no-op.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Prod orchestrator (8500): `/health` → `{"status":"ok"}`
|
||||
- Дата: 2026-06-07
|
||||
|
||||
## Результаты (контракт-тесты `04-test-plan.yaml`)
|
||||
|
||||
| TC ID | Описание | Тест | Результат |
|
||||
|-------|----------|------|-----------|
|
||||
| TC-01 | `_PLANE_NAME_TO_KEY`: `'Confirm Deploy' → 'confirm_deploy'` | test_tc01_confirm_deploy_name_to_key_mapping; test_tc01_confirm_deploy_not_in_default_states | PASS |
|
||||
| TC-02 | `get_project_states` ORCH резолвит непустой UUID под `confirm_deploy`, ≠ `approved` | test_tc02_get_project_states_resolves_confirm_deploy | PASS |
|
||||
| TC-03 | Fail-closed при отсутствии статуса (API недоступен / нет на доске) — без исключения | test_tc03_fail_closed_when_api_unreachable; test_tc03_fail_closed_when_status_not_on_board | PASS |
|
||||
| TC-04 | `handle_issue_updated`: `Confirm Deploy` на `deploy` → путь Фазы B | test_tc04_confirm_deploy_routes_phase_b; test_tc04b_confirm_deploy_off_deploy_stage_is_noop | PASS |
|
||||
| TC-05 | `Approved` на `deploy` НЕ вызывает `initiate_deploy` | test_tc05_approved_on_deploy_does_not_initiate | PASS |
|
||||
| TC-06 | `Approved` на `analysis` по-прежнему продвигает → architecture | test_tc06_approved_on_analysis_still_advances | PASS |
|
||||
| TC-07 | stage_engine: Фаза B только по confirm-deploy; `Approved` → no-op | test_tc07_confirm_deploy_initiates; test_tc07_approved_without_confirm_is_noop | PASS |
|
||||
| TC-08 | Идемпотентность: повтор `Confirm Deploy` при маркере `initiated` → no-op | test_tc08_idempotent_repeat_confirm_deploy | PASS |
|
||||
| TC-09 | CTA Фазы A содержит «Confirm Deploy», не предлагает «Approved» как триггер | test_tc09_phase_a_cta_requests_confirm_deploy | PASS |
|
||||
| TC-10 | E2E: `Confirm Deploy` → `initiate_deploy` вызван, finalizer поставлен, маркер записан | test_tc10_confirm_deploy_e2e_initiates | PASS |
|
||||
| TC-11 | E2E: `Approved` → деплой НЕ инициирован, задача остаётся на `deploy` | test_tc11_approved_e2e_noop | PASS |
|
||||
| TC-12 | Условность: не-self репо verdict-статусы не меняют поведение деплоя | test_tc12_non_self_repo_unaffected | PASS |
|
||||
| RG-01 | Полный регресс зелёный; STAGE_TRANSITIONS / QG_CHECKS без изменений | tests/ (763 passed) | PASS |
|
||||
|
||||
Все 16 целевых тестов ORCH-059 (TC-01..TC-12) — PASS.
|
||||
|
||||
## Сопоставление с критериями приёмки (`03-acceptance-criteria.md`)
|
||||
|
||||
| AC | Покрытие | Результат |
|
||||
|----|----------|-----------|
|
||||
| AC-1 Статус резолвится | TC-01, TC-02 | PASS |
|
||||
| AC-2 Confirm Deploy на `deploy` → Фаза B | TC-04, TC-07, TC-10 | PASS |
|
||||
| AC-3 Approved на `deploy` НЕ деплоит | TC-05, TC-07, TC-11 | PASS |
|
||||
| AC-4 Approved на `analysis` без регрессии | TC-06 | PASS |
|
||||
| AC-5 Идемпотентность Фазы B | TC-08 | PASS |
|
||||
| AC-6 CTA Фазы A просит Confirm Deploy | TC-09 | PASS |
|
||||
| AC-7 Fail-closed без статуса | TC-03 | PASS |
|
||||
| AC-8 Условность для не-self | TC-12 | PASS |
|
||||
| AC-9 Инварианты, pytest зелёный | RG-01 (763 passed) | PASS |
|
||||
| AC-10 Документация обновлена | проверено reviewer (12-review.md, APPROVED) | PASS |
|
||||
|
||||
## Smoke test API (prod 8500)
|
||||
- `GET /health` → `{"status":"ok","service":"orchestrator"}`
|
||||
- `GET /status` → 200, активные задачи отдаются (вкл. ORCH-059 на `testing`)
|
||||
- `GET /queue` → 200, counts + resilience + reconcile + reaper + post_deploy
|
||||
|
||||
## Вывод pytest
|
||||
```
|
||||
======================= 763 passed, 1 warning in 15.45s ========================
|
||||
```
|
||||
Целевой набор ORCH-059:
|
||||
```
|
||||
======================== 16 passed, 1 warning in 0.75s =========================
|
||||
```
|
||||
(1 warning — PydanticDeprecatedSince20 в `src/config.py`, не относится к ORCH-059.)
|
||||
|
||||
## Итог
|
||||
**PASS** — все контракт-тесты (TC-01..TC-12) и регресс (763 passed) зелёные,
|
||||
критерии приёмки AC-1..AC-10 покрыты, smoke API OK. Задача готова к стадии
|
||||
deploy-staging.
|
||||
12
docs/work-items/ORCH-059/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-059/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-059
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
29
docs/work-items/ORCH-059/15-staging-log.md
Normal file
29
docs/work-items/ORCH-059/15-staging-log.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T19:19:25Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed. Verdict: **SUCCESS** (exit 0).
|
||||
|
||||
Canonical run inside the `orchestrator-staging` container (ORCH-048, ADR-001):
|
||||
`python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`
|
||||
|
||||
## Result
|
||||
|
||||
- RESULT: 8/10 checks PASS
|
||||
- REAL failed: none
|
||||
- SANDBOX_INFRA failed: C9a (branch in orchestrator-sandbox), C9b (analyst job enqueued)
|
||||
|
||||
All REAL pipeline checks (Block A SMOKE, Block B ACCESS incl. B6 registry isolation,
|
||||
C7/C8) are green. The two failing checks are sandbox-infra-only (SANDBOX bot accounts
|
||||
not members of the SANDBOX Plane project) and were waived per ORCH-061. Exit code 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
|
||||
```
|
||||
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
14
docs/work-items/ORCH-059/16-post-deploy-log.md
Normal file
14
docs/work-items/ORCH-059/16-post-deploy-log.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY
|
||||
action_taken: NONE
|
||||
work_item: ORCH-059
|
||||
window_s: 900
|
||||
checks_total: 30
|
||||
checks_failed: 0
|
||||
---
|
||||
|
||||
# Post-deploy log — ORCH-021 post-deploy monitor
|
||||
|
||||
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
|
||||
|
||||
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.
|
||||
7
docs/work-items/ORCH-065/00-business-request.md
Normal file
7
docs/work-items/ORCH-065/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: BUG: zombie jobs + merge-lease залип (процесс умер, статус running)
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
103
docs/work-items/ORCH-065/01-brd.md
Normal file
103
docs/work-items/ORCH-065/01-brd.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# BRD — ORCH-065: zombie jobs + залипший merge-lease
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
Тип: BUG (P0)
|
||||
Репозиторий: orchestrator (self-hosting)
|
||||
Эпик: блокер ORCH-54 (полностью автономный self-deploy)
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`,
|
||||
`max_concurrency=1` для self-hosting), обслуживающий несколько проектов. Финальная
|
||||
автономность self-deploy упирается в два связанных класса отказов, оба сводящиеся
|
||||
к «процесс умер/завершился, а состояние осталось захваченным навсегда»:
|
||||
|
||||
### Проблема A — zombie jobs (строка `jobs` навсегда `running`)
|
||||
Агент (deployer/developer/reviewer) завершается **или умирает** (краш, OOM,
|
||||
рестарт контейнера в ходе self-deploy, гибель monitor-потока), но строка в таблице
|
||||
`jobs` остаётся в статусе `running`. Финализация статуса job выполняется **только**
|
||||
в `_monitor_agent` → `_finalize_job` внутри того же процесса; если этот поток/процесс
|
||||
не доживает до финализации — job «зомбирован».
|
||||
|
||||
- Единственная имеющаяся защита — `requeue_running_jobs()` в `main.lifespan`,
|
||||
срабатывающая **исключительно на старте процесса**. Зомби, возникший **без**
|
||||
рестарта (умер дочерний процесс/monitor-поток, а сервис жив), не реанимируется
|
||||
никогда.
|
||||
- При `max_concurrency=1` одна зомби-строка `running` блокирует claim всех
|
||||
последующих job (`count_running_jobs() >= max_concurrency` → claim не происходит)
|
||||
→ **встаёт конвейер всех проектов**.
|
||||
|
||||
### Проблема B — залипший merge-lease
|
||||
Merge-gate (ORCH-043) берёт файловый lease `<repos_dir>/.merge-lease-<repo>.json`
|
||||
ПЕРЕД rebase+re-test и держит его до фактического merge PR в `main`. Если процесс
|
||||
умирает на финальном merge **с зажатым lease**:
|
||||
|
||||
- Реклейм lease реализован **лениво и только по возрасту** (`age >=
|
||||
merge_lock_timeout_s`) и **только в момент `acquire_merge_lease` другой задачей**.
|
||||
Проактивного освобождения (на старте / периодически) нет; **liveness держателя по
|
||||
pid не проверяется** (хотя `pid` в lease пишется).
|
||||
- Пострадавшая задача сама re-drive не получает: merge не финализируется → задача
|
||||
висит, lease мешает чужим merge до истечения TTL.
|
||||
|
||||
### Проблема C — неидемпотентная финализация merge
|
||||
Если rebase+re-test прошли зелёно (ветка догнана и проверена), но процесс умер до
|
||||
завершения слияния PR — повторного «докатывания» merge нет. Задача застревает в
|
||||
полу-выполненном состоянии, хотя вся дорогая работа (rebase+re-test) уже сделана.
|
||||
|
||||
## 2. Бизнес-последствия
|
||||
- **Это ПОСЛЕДНЯЯ ручная точка автономного деплоя.** Без фикса ни одна self-hosting
|
||||
задача не доезжает до прода без оператора (cancel zombie + ручной merge PR +
|
||||
ручной `--deploy`).
|
||||
- Прямой блокер эпика ORCH-54.
|
||||
- Доказанные инциденты (07.06): ORCH-58/60/61/21 — каждый раз после успешного
|
||||
deployer-прохода job оставался `running`; jobs **236/239/242/254** — зомби,
|
||||
прод-merge/deploy доводились вручную.
|
||||
- Групповой риск: зомби в общей очереди при concurrency=1 останавливает конвейер
|
||||
enduro-trails и всех прочих проектов.
|
||||
|
||||
## 3. Цель
|
||||
Сделать так, чтобы **смерть процесса/потока на любой стадии (включая self-restart
|
||||
во время deploy) НЕ оставляла навсегда захваченных ресурсов** — ни строки `jobs` в
|
||||
`running`, ни merge-lease. Конвейер должен самовосстанавливаться без оператора, при
|
||||
этом сохраняя все инварианты self-hosting (не ронять прод-контейнер, не трогать
|
||||
`main`, fail-closed на реальных ошибках).
|
||||
|
||||
## 4. Объём (Scope)
|
||||
|
||||
### В объёме
|
||||
1. **Job-reaper** — фоновый watchdog (паттерн `reconciler`/`queue_worker`),
|
||||
детектирующий «мёртвый» `running`-job и приводящий его строку в корректный
|
||||
терминальный/повторный статус (`done`/`failed`/`queued`) детерминированно,
|
||||
без LLM. Restart-safe и работающий **без** рестарта процесса.
|
||||
2. **Проактивный реклейм stale merge-lease** — освобождение lease, чей держатель
|
||||
мёртв (pid не жив) ИЛИ возраст превысил TTL — на старте и периодически (reaper/
|
||||
reconciler), а не только лениво при чужом `acquire`.
|
||||
3. **Идемпотентная финализация merge** — если rebase+re-test зелёные, но merge не
|
||||
состоялся, операция повторяется/докатывается без потери уже сделанной работы.
|
||||
|
||||
### Вне объёма
|
||||
- Переход на внешний брокер очередей / смену схемы блокировок merge на БД-lock.
|
||||
- Полный авто-approve деплоя (ORCH-54) — отдельная задача; здесь только снятие
|
||||
технического блокера.
|
||||
- Изменение конвейера стадий (`STAGE_TRANSITIONS`) и реестра гейтов как контрактов.
|
||||
|
||||
## 5. Заинтересованные стороны
|
||||
- Owner оркестратора (self-hosting автономность).
|
||||
- Все проекты на общем инстансе (enduro-trails и пр.) — страдают от блокировки
|
||||
общей очереди.
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
- `max_concurrency=1` для self-hosting сохраняется.
|
||||
- Self-hosting safety (CLAUDE.md): нельзя ронять/рестартить прод-контейнер в рамках
|
||||
задачи; нельзя пушить/форс-пушить `main`; реклейм lease не должен прерывать
|
||||
легитимно работающий merge.
|
||||
- Никаких ложных реанимаций: живой, но долгий job не должен помечаться зомби
|
||||
(нужен порог/грейс «N тиков» + проверка реальной смерти, а не просто долготы).
|
||||
- Контракт **never-raise** для всей новой фоновой логики (как у reconciler/merge_gate).
|
||||
- Kill-switch на каждый новый механизм (как `reconcile_enabled` / `merge_gate_enabled`).
|
||||
|
||||
## 7. Критерий успеха (бизнес-уровень)
|
||||
После фикса воспроизводимый сценарий «успешный deployer-проход + смерть процесса/
|
||||
self-restart» НЕ оставляет зомби-job и зажатого lease: задача либо корректно
|
||||
доезжает до `done` сама, либо откатывается по штатному контракту — **без участия
|
||||
оператора**. Регресс-тест на jobs-зомби и stale-lease зелёный.
|
||||
170
docs/work-items/ORCH-065/02-trz.md
Normal file
170
docs/work-items/ORCH-065/02-trz.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# ТЗ — ORCH-065: job-reaper + stale merge-lease reclaim + идемпотентный merge
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
Базируется на: 01-brd.md
|
||||
Примечание архитектору: ТЗ фиксирует ТРЕБОВАНИЯ и кандидатные модули. Выбор
|
||||
конкретной реализации (новый модуль vs расширение reconciler; колонка `jobs.pid`
|
||||
vs эвристика на `agent_runs`) — за стадией architecture (ADR). Если какая-либо
|
||||
часть ТЗ окажется нереализуемой/спорной — вернуть в Анализ, не комментировать
|
||||
задним числом.
|
||||
|
||||
## 0. Текущее состояние (факты из кода)
|
||||
|
||||
| Место | Факт |
|
||||
|-------|------|
|
||||
| `src/queue_worker.py` `_drain_once` | claim не происходит, пока `count_running_jobs() >= max_concurrency`. Одна зомби-строка `running` при concurrency=1 блокирует всю очередь. |
|
||||
| `src/agents/launcher.py` `_monitor_agent` → `_finalize_job` | статус job (`done`/`queued`/`failed`) выставляется ТОЛЬКО в этом monitor-потоке. Смерть потока/процесса до финализации ⇒ job навсегда `running`. |
|
||||
| `src/main.py` (lifespan, строки ~55-61) | `requeue_running_jobs()` вызывается ТОЛЬКО при старте процесса. |
|
||||
| `src/db.py` `requeue_running_jobs` | flip всех `running`→`queued`. Без рестарта не запускается. |
|
||||
| `src/db.py` таблица `jobs` | колонки `pid`/`heartbeat` НЕТ; есть `run_id`, `started_at`, `attempts`, `max_attempts`. |
|
||||
| `src/merge_gate.py` `acquire_merge_lease` | реклейм stale lease (age `>= merge_lock_timeout_s`) и corrupt — ТОЛЬКО лениво в момент `acquire`. Lease пишет `pid`, но liveness по pid нигде не проверяется. |
|
||||
| `src/merge_gate.py` `release_merge_lease` | holder-aware (по `branch`), идемпотентен. Вызовы: `webhooks/gitea.py:380` (PR-merged), `stage_engine.py:352/740/876/952`, `qg/checks.py:683/691/697`. |
|
||||
| `src/qg/checks.py` `check_branch_mergeable` | при SUCCESS lease ДЕРЖИТСЯ до фактического merge PR. Если процесс умрёт после этого — lease зажат. |
|
||||
| `src/reconciler.py` | паттерн-образец фонового daemon-потока (never-raise, kill-switch, observability в `/queue`). |
|
||||
|
||||
## 1. Задействованные модули `src/`
|
||||
|
||||
- `src/db.py` — новые job-запросы для reaper (выборка stale running-job, атомарный
|
||||
reap). Возможна lightweight-миграция (см. §3).
|
||||
- **Job-reaper** — НОВЫЙ модуль (кандидат `src/job_reaper.py`) ИЛИ расширение
|
||||
`src/reconciler.py`. Решение — за архитектором; ТЗ требует daemon-поток по образцу
|
||||
`reconciler` (never-raise, `_stop`-Event, старт/стоп в `main.lifespan`, снимок в
|
||||
`/queue`).
|
||||
- `src/merge_gate.py` — функция проактивного реклейма stale/dead lease (по pid-
|
||||
liveness + по TTL); helper проверки liveness pid; helper идемпотентной
|
||||
финализации merge.
|
||||
- `src/main.py` — старт/стоп нового daemon-потока в `lifespan` (после `worker.start()`
|
||||
/ `reconciler.start()`, симметрично остановка перед `worker.stop()`); вызов
|
||||
стартового реклейма stale-lease рядом с `requeue_running_jobs()`.
|
||||
- `src/config.py` — новые настройки/флаги (см. §5).
|
||||
- `src/main.py` `GET /queue` — блок наблюдаемости reaper (образец `reconcile`/
|
||||
`post_deploy`).
|
||||
|
||||
## 2. Функциональные требования
|
||||
|
||||
### FR-1. Job-reaper (Проблема A)
|
||||
- Фоновый поток периодически (`reaper_interval_s`) сканирует строки `jobs` в статусе
|
||||
`running`.
|
||||
- Для каждого `running`-job определяет, **жив ли его исполнитель**. «Мёртвым» job
|
||||
считается, когда выполнено и устойчиво (см. FR-1.3) хотя бы одно из:
|
||||
- процесс агента (по pid/run_id) не существует, а финализация не произошла;
|
||||
- `agent_runs` строки run_id имеет `finished_at`/`exit_code` (процесс реально
|
||||
завершился), но `jobs.status` всё ещё `running` (monitor умер между записью
|
||||
exit_code и `_finalize_job`);
|
||||
- job висит `running` дольше предохранительного потолка
|
||||
`reaper_max_running_s` (заведомо больше любого легитимного `agent_timeout` +
|
||||
grace) — backstop на случай, когда liveness определить нельзя.
|
||||
- FR-1.2 Действие при подтверждённой смерти:
|
||||
- если есть достоверный успешный исход (`agent_runs.exit_code == 0`) — довести job
|
||||
к корректному завершению **через тот же контракт**, что `_finalize_job`
|
||||
(включая, при необходимости, повторную попытку auto-advance) — НЕ дублировать
|
||||
переход, если он уже произошёл (идемпотентность через `has_active_job_for_task`
|
||||
/ проверку стадии);
|
||||
- если исход неуспешный/неизвестен и бюджет попыток не исчерпан
|
||||
(`attempts < max_attempts`) — `queued` (повторная постановка), как делает
|
||||
`requeue_running_jobs`;
|
||||
- если бюджет исчерпан — `failed` + Telegram-алерт.
|
||||
- FR-1.3 **Анти-ложноположительность.** Job помечается зомби только после
|
||||
устойчивого подтверждения смерти: процесс мёртв на протяжении `reaper_dead_ticks`
|
||||
последовательных тиков (≥2) ИЛИ превышен `reaper_max_running_s`. Живой долгий
|
||||
агент (в пределах своего `agent_timeout`) НИКОГДА не реапится.
|
||||
- FR-1.4 Работает **без рестарта** процесса (главное отличие от существующего
|
||||
`requeue_running_jobs`).
|
||||
- FR-1.5 Restart-safe: после рестарта поведение корректно совмещается со стартовым
|
||||
`requeue_running_jobs()` (нет двойной обработки одной строки; атомарность reap-
|
||||
UPDATE с guard по `status='running'`, как в `claim_next_job`).
|
||||
|
||||
### FR-2. Проактивный реклейм stale/dead merge-lease (Проблема B)
|
||||
- FR-2.1 На старте процесса (рядом с `requeue_running_jobs()` в `lifespan`) и
|
||||
периодически в фоновом потоке: для каждого репо с merge-gate проверить lease и
|
||||
освободить его, если держатель **мёртв** или lease **просрочен**.
|
||||
- FR-2.2 «Держатель мёртв» = pid из lease не существует в системе (liveness-проба,
|
||||
напр. `os.kill(pid, 0)` с обработкой `ProcessLookupError`/`PermissionError`),
|
||||
при условии что pid принадлежит этому хосту/неймспейсу. «Просрочен» = `age >=
|
||||
merge_lock_timeout_s` (существующий TTL-контракт сохраняется).
|
||||
- FR-2.3 Реклейм **holder-aware и безопасен**: НЕ освобождать lease, чей держатель
|
||||
жив и в пределах TTL (защита легитимного merge). Логировать `warning` при каждом
|
||||
реклейме (наблюдаемость, как сейчас в `acquire_merge_lease`).
|
||||
- FR-2.4 Условность как ORCH-35/43: реально только для self-hosting/`merge_gate_repos`;
|
||||
прочие репо — no-op.
|
||||
- FR-2.5 Контракт **never-raise**; любая ошибка реклейма не должна валить поток.
|
||||
|
||||
### FR-3. Идемпотентная финализация merge (Проблема C)
|
||||
- FR-3.1 Если ветка прошла rebase+re-test (догнана до `origin/main` и зелёная), но
|
||||
merge PR не состоялся из-за смерти процесса — система должна **докатить/повторить**
|
||||
merge без повторного прогона дорогих шагов, когда это безопасно.
|
||||
- FR-3.2 Финализация merge должна быть **идемпотентной**: повторный вызов при уже
|
||||
слитом PR — no-op (определять по состоянию PR в Gitea и/или по
|
||||
`branch_is_behind_main`/состоянию `main`), без ошибки и без второго слияния.
|
||||
- FR-3.3 Восстановление re-drive обеспечивается штатными механизмами (reaper
|
||||
довёл job до `queued` → повторный проход стадии `deploy`/merge-gate; либо
|
||||
reconciler доигрывает переход). Дублирующая логика merge НЕ создаётся — переиспользуются
|
||||
существующие пути (`check_branch_mergeable` / deployer-merge).
|
||||
- FR-3.4 При повторе lease берётся заново (идемпотентный re-acquire «held by self»
|
||||
по branch уже поддержан в `acquire_merge_lease`).
|
||||
|
||||
### FR-4. Наблюдаемость
|
||||
- FR-4.1 Блок `reaper` в `GET /queue`: enabled, interval, last_run_ts, reaped_total,
|
||||
last_reaped (job_id/agent), lease_reclaimed_total (best-effort, как у reconciler).
|
||||
- FR-4.2 Каждый reap и каждый lease-reclaim — `logger.warning` с идентификаторами
|
||||
(job_id, run_id, pid, repo, branch).
|
||||
- FR-4.3 При reap→`failed` и при lease-reclaim — Telegram (как существующие алерты).
|
||||
|
||||
## 3. Изменения схемы БД
|
||||
- Текущая `jobs` НЕ содержит `pid`. Для надёжной pid-liveness job-reaper'у, скорее
|
||||
всего, потребуется **lightweight-миграция**: добавить `jobs.pid INTEGER` (через
|
||||
`_ensure_column`, идемпотентно, безопасно на live prod DB — паттерн уже
|
||||
применяется в `db.py`). pid проставляется в `_spawn` рядом с `run_id`/`started_at`.
|
||||
- **Альтернатива без миграции** (на усмотрение архитектора): определять смерть по
|
||||
`agent_runs.finished_at/exit_code` + потолку `reaper_max_running_s`, без хранения
|
||||
pid в `jobs`. ADR должен зафиксировать выбор и обоснование.
|
||||
- Реестры `STAGE_TRANSITIONS` и `QG_CHECKS` — **без изменений** (новых стадий/гейтов
|
||||
не вводим; reaper и lease-reclaim — фоновые механизмы, не стадии).
|
||||
- Merge-lease остаётся файловым (`.merge-lease-<repo>.json`); схема файла lease
|
||||
не меняется (pid и acquired_at уже есть).
|
||||
|
||||
## 4. Изменения API
|
||||
- `GET /queue` — добавить блок `reaper` (read-only наблюдаемость). Прочие endpoints
|
||||
без изменений. Новых webhook-роутов нет.
|
||||
|
||||
## 5. Конфигурация / kill-switches (`src/config.py`)
|
||||
Именование — по образцу `reconcile_*` / `merge_*`. Кандидаты (точные имена/дефолты
|
||||
уточняет архитектор):
|
||||
|
||||
| Настройка | Назначение | Дефолт (предложение) |
|
||||
|-----------|-----------|----------------------|
|
||||
| `reaper_enabled` | глобальный kill-switch job-reaper | `true` |
|
||||
| `reaper_interval_s` | период сканирования | `60` |
|
||||
| `reaper_dead_ticks` | сколько подряд тиков pid должен быть мёртв перед reap | `2` |
|
||||
| `reaper_max_running_s` | потолок «running» (backstop), > max agent_timeout+grace | `3600` |
|
||||
| `lease_reclaim_enabled` | kill-switch проактивного реклейма lease | `true` |
|
||||
| (переиспользуется) `merge_lock_timeout_s` | TTL lease | `300` (как есть) |
|
||||
| (переиспользуется) `merge_gate_repos` | область применения lease-reclaim | как есть |
|
||||
|
||||
Все флаги — пробрасываются из env (`ORCH_*`), `false` → строго прежнее поведение.
|
||||
|
||||
## 6. Требования к QG checks
|
||||
- Новых QG checks НЕ вводить (это фоновые resilience-механизмы, не гейты выхода со
|
||||
стадии). `check_branch_mergeable` остаётся контрактно неизменным; допускается лишь
|
||||
переиспользование его как идемпотентного пути финализации merge (FR-3.3).
|
||||
|
||||
## 7. Артефакты pipeline, создаваемые/обновляемые в ЭТОМ PR
|
||||
- Код: см. §1.
|
||||
- `06-adr/ADR-001-*.md` — архитектурное решение (где живёт reaper; pid-колонка vs
|
||||
эвристика; механизм идемпотентного merge) — создаёт architect.
|
||||
- `docs/architecture/README.md` — новый раздел про job-reaper + lease-reclaim
|
||||
(golden-source, в этом же PR).
|
||||
- `docs/architecture/internals.md` — детали (если затрагивается схема БД / потоки).
|
||||
- `CHANGELOG.md` — запись ORCH-065.
|
||||
- `.env.example` — новые `ORCH_*` флаги (канон секретов/настроек).
|
||||
- `docs/operations/INFRA.md` — упоминание поведения при self-restart, если
|
||||
затрагивается (best-effort).
|
||||
|
||||
## 8. Инварианты (НЕ нарушать)
|
||||
- Не ронять/не рестартить прод-контейнер `orchestrator` в рамках задачи.
|
||||
- Никогда не пушить/форс-пушить `main`; реклейм lease не инициирует git-операций.
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, контракты `check_*`, БАГ-8 откат,
|
||||
exit-коды deploy-хука — без изменений.
|
||||
- never-raise на единицу фоновой работы; идемпотентность; restart-safe; тишина при
|
||||
отсутствии аномалий (как reconciler).
|
||||
- Анти-ложноположительность (FR-1.3): живой долгий агент не реапится.
|
||||
122
docs/work-items/ORCH-065/03-acceptance-criteria.md
Normal file
122
docs/work-items/ORCH-065/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Критерии приёмки — ORCH-065
|
||||
|
||||
Work Item ID: ORCH-065
|
||||
Формат: каждый критерий имеет явное условие PASS/FAIL. Все критерии должны быть
|
||||
PASS для прохождения review/testing.
|
||||
|
||||
## A. Job-reaper (Проблема A)
|
||||
|
||||
### AC-1 — реап мёртвого running-job без рестарта
|
||||
- PASS: при наличии строки `jobs` в статусе `running`, чей процесс/исполнитель
|
||||
достоверно мёртв (pid не существует ИЛИ `agent_runs.exit_code` записан, а job всё
|
||||
ещё `running`) и условие устойчивости (FR-1.3) выполнено, фоновый reaper переводит
|
||||
строку в корректный статус (`done`/`queued`/`failed`) **без перезапуска процесса**.
|
||||
- FAIL: строка остаётся `running` после `reaper_dead_ticks` тиков / превышения
|
||||
`reaper_max_running_s`.
|
||||
|
||||
### AC-2 — разблокировка очереди при concurrency=1
|
||||
- PASS: после реапа зомби-строки `count_running_jobs()` снижается, и следующий
|
||||
queued-job успешно claim'ится воркером.
|
||||
- FAIL: очередь остаётся заблокированной зомби-строкой.
|
||||
|
||||
### AC-3 — анти-ложноположительность (живой долгий агент не реапится)
|
||||
- PASS: `running`-job с ЖИВЫМ процессом в пределах его `agent_timeout` НЕ помечается
|
||||
зомби (ни по одному тику, ни в пределах `reaper_max_running_s`, если потолок
|
||||
больше таймаута).
|
||||
- FAIL: живой агент помечен `failed`/`queued` reaper'ом.
|
||||
|
||||
### AC-4 — корректный исход по результату
|
||||
- PASS: при `agent_runs.exit_code == 0` reaper доводит до успешного завершения без
|
||||
дублирования уже выполненного stage-advance (идемпотентно); при неуспехе и
|
||||
`attempts < max_attempts` → `queued`; при исчерпании → `failed` + Telegram.
|
||||
- FAIL: успешный исход помечен `failed`; либо дублируется stage-переход; либо
|
||||
исчерпанный бюджет молча зацикливается на `queued`.
|
||||
|
||||
### AC-5 — restart-safe совместимость
|
||||
- PASS: одновременная работа стартового `requeue_running_jobs()` и периодического
|
||||
reaper не приводит к двойной обработке одной строки (атомарный UPDATE с guard
|
||||
`status='running'`).
|
||||
- FAIL: одна строка обработана дважды / гонка приводит к рассинхрону статуса.
|
||||
|
||||
## B. Stale/dead merge-lease reclaim (Проблема B)
|
||||
|
||||
### AC-6 — реклейм lease мёртвого держателя
|
||||
- PASS: lease `.merge-lease-<repo>.json`, чей `pid` не существует, проактивно
|
||||
освобождается на старте И периодическим потоком (не дожидаясь TTL и не дожидаясь
|
||||
чужого `acquire`).
|
||||
- FAIL: lease мёртвого держателя остаётся до истечения `merge_lock_timeout_s` или
|
||||
до следующего чужого `acquire`.
|
||||
|
||||
### AC-7 — реклейм по TTL сохранён
|
||||
- PASS: lease старше `merge_lock_timeout_s` освобождается (существующий контракт не
|
||||
сломан), с `logger.warning`.
|
||||
- FAIL: просроченный lease не освобождается.
|
||||
|
||||
### AC-8 — не трогать живой lease
|
||||
- PASS: lease с ЖИВЫМ держателем (pid жив) и возрастом `< merge_lock_timeout_s` НЕ
|
||||
освобождается (защита легитимного merge).
|
||||
- FAIL: освобождён lease живого держателя → возможен параллельный конфликтный merge.
|
||||
|
||||
### AC-9 — условность и never-raise
|
||||
- PASS: реклейм реален только для `merge_gate_repos`/self-hosting; для прочих репо
|
||||
— no-op; любая ошибка реклейма логируется и не валит поток (never-raise).
|
||||
- FAIL: реклейм выполняется для не-self-hosting репо; либо ошибка пробрасывается
|
||||
наружу/роняет поток.
|
||||
|
||||
## C. Идемпотентная финализация merge (Проблема C)
|
||||
|
||||
### AC-10 — докатывание незавершённого merge
|
||||
- PASS: сценарий «rebase+re-test зелёные, merge не состоялся, процесс умер»
|
||||
восстанавливается автоматически (job → `queued` reaper'ом / reconciler доигрывает),
|
||||
и merge доводится без повторного ненужного прогона дорогих шагов.
|
||||
- FAIL: задача остаётся в полу-выполненном состоянии, требует ручного merge.
|
||||
|
||||
### AC-11 — идемпотентность при уже слитом PR
|
||||
- PASS: повторный вызов финализации при уже слитом PR — no-op (определяется по
|
||||
состоянию PR/`main`), без ошибки и без второго merge.
|
||||
- FAIL: второй merge / ошибка при уже слитом PR.
|
||||
|
||||
## D. Инварианты и безопасность self-hosting
|
||||
|
||||
### AC-12 — прод-контейнер не трогается
|
||||
- PASS: ни reaper, ни lease-reclaim не рестартят/не роняют прод-контейнер и не
|
||||
инициируют git-push в `main`.
|
||||
- FAIL: любая из новых веток кода рестартит self / пушит main.
|
||||
|
||||
### AC-13 — контракты неизменны
|
||||
- PASS: `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, сигнатуры/поведение `check_*`,
|
||||
БАГ-8 откат, exit-коды deploy-хука — без изменений; новых QG checks/стадий нет.
|
||||
- FAIL: затронут любой из перечисленных контрактов.
|
||||
|
||||
### AC-14 — kill-switches
|
||||
- PASS: `reaper_enabled=false` → reaper не работает (строго прежнее поведение);
|
||||
`lease_reclaim_enabled=false` → проактивный реклейм отключён (остаётся лишь
|
||||
прежний ленивый TTL-реклейм в `acquire`).
|
||||
- FAIL: флаг `false` не отключает соответствующий механизм.
|
||||
|
||||
## E. Наблюдаемость
|
||||
|
||||
### AC-15 — блок reaper в /queue
|
||||
- PASS: `GET /queue` содержит блок `reaper` (enabled, interval, last_run_ts,
|
||||
reaped_total, last_reaped, lease_reclaimed_total).
|
||||
- FAIL: блок отсутствует/не обновляется.
|
||||
|
||||
### AC-16 — логи и алерты
|
||||
- PASS: каждый reap и lease-reclaim → `logger.warning` с идентификаторами;
|
||||
reap→`failed` и lease-reclaim → Telegram.
|
||||
- FAIL: реап/реклейм происходят молча.
|
||||
|
||||
## F. Документация (gate reviewer)
|
||||
|
||||
### AC-17 — golden-source обновлён в этом же PR
|
||||
- PASS: обновлены `docs/architecture/README.md` (раздел про reaper + lease-reclaim),
|
||||
`CHANGELOG.md`, `.env.example` (новые `ORCH_*` флаги); заведён
|
||||
`06-adr/ADR-001-*.md`.
|
||||
- FAIL: код изменён, документация — нет (reviewer → REQUEST_CHANGES).
|
||||
|
||||
## G. Тесты
|
||||
|
||||
### AC-18 — регресс-тесты зелёные
|
||||
- PASS: новые unit/integration тесты (см. 04-test-plan.yaml) проходят; существующий
|
||||
`pytest tests/ -q` зелёный (нет регресса merge_gate / queue / reconciler).
|
||||
- FAIL: любой тест из плана красный или сломан существующий тест.
|
||||
196
docs/work-items/ORCH-065/04-test-plan.yaml
Normal file
196
docs/work-items/ORCH-065/04-test-plan.yaml
Normal file
@@ -0,0 +1,196 @@
|
||||
work_item: ORCH-065
|
||||
description: >
|
||||
Тест-план для фикса zombie jobs (job-reaper), залипшего merge-lease
|
||||
(проактивный реклейм dead/stale lease) и идемпотентной финализации merge.
|
||||
Все новые фоновые механизмы — never-raise, restart-safe, kill-switch.
|
||||
Модуль reaper и точные имена функций уточнит архитектор; в module указаны
|
||||
кандидатные пути (tests/test_job_reaper.py / tests/test_merge_lease_reclaim.py).
|
||||
|
||||
tests:
|
||||
# ---- A. Job-reaper ----
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: >
|
||||
Reaper переводит running-job с мёртвым исполнителем в корректный статус
|
||||
без рестарта процесса (pid не существует / exit_code записан, job всё ещё
|
||||
running). Покрывает AC-1.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
Анти-ложноположительность: running-job с ЖИВЫМ процессом в пределах
|
||||
agent_timeout НЕ реапится (ни по одному тику, ни в пределах reaper_max_running_s).
|
||||
Покрывает AC-3.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
Устойчивость: job помечается зомби только после reaper_dead_ticks
|
||||
последовательных тиков смерти pid (>=2), а не на первом тике. Покрывает FR-1.3.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >
|
||||
Backstop по потолку: job, висящий running дольше reaper_max_running_s,
|
||||
реапится даже если liveness определить нельзя. Покрывает FR-1.1/AC-1.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: >
|
||||
Корректный исход: exit_code==0 -> успешное завершение без дублирования
|
||||
stage-advance; неуспех при attempts<max -> queued; исчерпан бюджет -> failed
|
||||
+ Telegram. Покрывает AC-4.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: >
|
||||
Атомарность reap-UPDATE с guard status='running': параллельная обработка
|
||||
одной строки (стартовый requeue_running_jobs + reaper) не приводит к двойному
|
||||
reap. Покрывает AC-5.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: >
|
||||
Kill-switch reaper_enabled=false -> reaper не трогает ни одной строки
|
||||
(строго прежнее поведение). Покрывает AC-14.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: >
|
||||
never-raise: ошибка БД/ОС внутри одного тика reaper не пробрасывается и не
|
||||
останавливает поток (изоляция per-job, образец reconciler). Покрывает AC-9.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: >
|
||||
Очередь разблокируется: после reap зомби-строки при max_concurrency=1
|
||||
следующий queued-job успешно claim'ится (claim_next_job + count_running_jobs).
|
||||
Покрывает AC-2.
|
||||
module: tests/test_queue.py
|
||||
expected: PASS
|
||||
|
||||
# ---- B. Stale/dead merge-lease reclaim ----
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: >
|
||||
Реклейм lease с мёртвым pid (os.kill(pid,0) -> ProcessLookupError)
|
||||
проактивно, не дожидаясь TTL и чужого acquire. Покрывает AC-6.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: >
|
||||
Реклейм по TTL (age >= merge_lock_timeout_s) сохранён, с logger.warning.
|
||||
Покрывает AC-7. (расширяет существующий stale-lease сценарий merge_gate.)
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: >
|
||||
Живой lease (pid жив, age < TTL) НЕ освобождается — защита легитимного merge.
|
||||
Покрывает AC-8.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: >
|
||||
Условность: реклейм реален только для merge_gate_repos/self-hosting; для
|
||||
прочих репо no-op. Покрывает AC-9.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: >
|
||||
never-raise: ошибка чтения/удаления lease-файла не валит реклейм-поток.
|
||||
Покрывает AC-9.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: >
|
||||
Kill-switch lease_reclaim_enabled=false -> проактивный реклейм отключён,
|
||||
остаётся лишь прежний ленивый TTL-реклейм в acquire_merge_lease.
|
||||
Покрывает AC-14.
|
||||
module: tests/test_merge_lease_reclaim.py
|
||||
expected: PASS
|
||||
|
||||
# ---- C. Идемпотентная финализация merge ----
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: >
|
||||
Идемпотентность финализации: повторный вызов при уже слитом PR / уже
|
||||
актуальном main (branch_is_behind_main == False) — no-op, без ошибки и без
|
||||
второго merge. Покрывает AC-11.
|
||||
module: tests/test_merge_gate.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: >
|
||||
Восстановление: сценарий "rebase+re-test зелёные, merge не состоялся,
|
||||
процесс умер" -> job доводится до queued reaper'ом и merge докатывается
|
||||
штатным путём без повторного дорогого re-test, когда безопасно. Покрывает AC-10.
|
||||
module: tests/test_merge_gate_race.py
|
||||
expected: PASS
|
||||
|
||||
# ---- D/E. Инварианты и наблюдаемость ----
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: >
|
||||
GET /queue содержит блок reaper (enabled, interval, last_run_ts,
|
||||
reaped_total, last_reaped, lease_reclaimed_total). Покрывает AC-15.
|
||||
module: tests/test_queue.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: unit
|
||||
description: >
|
||||
Контракты неизменны: STAGE_TRANSITIONS и реестр QG_CHECKS не получили новых
|
||||
стадий/чеков; check_branch_mergeable сигнатурно не изменён. Покрывает AC-13.
|
||||
module: tests/test_config.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-20
|
||||
type: unit
|
||||
description: >
|
||||
Новые настройки reaper_*/lease_reclaim_* присутствуют в config с дефолтами и
|
||||
читаются из ORCH_* env. Покрывает §5 ТЗ / AC-14.
|
||||
module: tests/test_config.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: >
|
||||
Стартовый реклейм stale/dead lease вызывается в lifespan рядом с
|
||||
requeue_running_jobs (smoke на регистрацию старт/стоп reaper-потока).
|
||||
Покрывает FR-2.1 / AC-6.
|
||||
module: tests/test_job_reaper.py
|
||||
expected: PASS
|
||||
|
||||
regression:
|
||||
- command: pytest tests/ -q
|
||||
expected: PASS
|
||||
note: >
|
||||
Полный прогон не должен ломать существующие тесты merge_gate / queue /
|
||||
reconciler / deploy.
|
||||
@@ -0,0 +1,275 @@
|
||||
# ADR-001 (ORCH-065): Job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge
|
||||
|
||||
## Статус
|
||||
Accepted — 2026-06-07
|
||||
|
||||
Связь со сквозным ADR: [docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md](../../../architecture/adr/adr-0011-job-reaper-lease-reclaim.md).
|
||||
|
||||
## Контекст
|
||||
|
||||
Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`,
|
||||
`max_concurrency=1` для self-hosting). BRD/ТЗ фиксируют три связанных класса
|
||||
отказов «процесс/поток умер, а состояние осталось захваченным навсегда»:
|
||||
|
||||
- **A — zombie jobs.** Статус job (`done`/`queued`/`failed`) выставляется ТОЛЬКО
|
||||
в `launcher._monitor_agent` → `_finalize_job` внутри того же процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляет строку `jobs` навсегда `running`.
|
||||
Единственная защита — `requeue_running_jobs()`, срабатывает ТОЛЬКО на старте
|
||||
процесса. Зомби без рестарта не реанимируется никогда. При `max_concurrency=1`
|
||||
одна зомби-строка блокирует claim всех job (`count_running_jobs() >=
|
||||
max_concurrency`) → встаёт конвейер ВСЕХ проектов. Доказано: jobs 236/239/242/254
|
||||
(07.06).
|
||||
- **B — залипший merge-lease.** Файловый lease `.merge-lease-<repo>.json`
|
||||
(ORCH-043) реклеймится **лениво и только по возрасту** (`age >=
|
||||
merge_lock_timeout_s`) и **только** в момент `acquire_merge_lease` другой
|
||||
задачей. Liveness держателя по pid не проверяется, хотя pid в lease пишется.
|
||||
Смерть с зажатым lease блокирует чужие merge до истечения TTL.
|
||||
- **C — неидемпотентная финализация merge.** Если rebase+re-test зелёные, но
|
||||
процесс умер до фактического merge PR — повторного докатывания нет; дорогая
|
||||
работа (rebase+re-test) сделана, а задача висит.
|
||||
|
||||
Факты кода, на которых строится решение:
|
||||
- `_spawn` (launcher.py:401) создаёт `subprocess.Popen(["bash","-c",cmd])`;
|
||||
`proc.pid` — pid агентского процесса, дочернего к процессу оркестратора в ОДНОМ
|
||||
pid-namespace контейнера. Сейчас `jobs.pid` НЕ хранится.
|
||||
- `_monitor_agent` (launcher.py:541) порядок: `proc.wait()` → запись
|
||||
`agent_runs.finished_at/exit_code` → git commit/push (+PR) → БАГ-8 deployer
|
||||
rollback → usage-комменты → `_try_advance_stage` (exit0, gate-driven advance)
|
||||
→ `_finalize_job` (драйв статуса job по контракту attempts/transient).
|
||||
- `claim_next_job` (db.py:454) — атомарный claim через `UPDATE ... WHERE id=? AND
|
||||
status='queued'` + `rowcount` (образец атомарности).
|
||||
- `reconciler.py` — образец фонового daemon-потока (never-raise, `_stop`-Event,
|
||||
старт/стоп в `main.lifespan`, снимок в `/queue`, kill-switch).
|
||||
- `merge_gate.py`: lease пишет `pid=os.getpid()` (pid процесса оркестратора, НЕ
|
||||
агента), `acquired_at`; `release_merge_lease` уже holder-aware (по `branch`) и
|
||||
идемпотентен; `acquire_merge_lease` идемпотентен для «held by self» (по branch).
|
||||
- `is_self_hosting_repo` / `merge_gate_repos` — образец условности (ORCH-35/43).
|
||||
|
||||
## Решение
|
||||
|
||||
### Р-1. Job-reaper — отдельный daemon-поток `src/job_reaper.py`
|
||||
|
||||
Reaper — **новый модуль и отдельный daemon-поток** (НЕ расширение reconciler).
|
||||
Обоснование: reconciler работает на уровне **stage-transition** (источник истины —
|
||||
гейт/Plane); reaper работает на уровне **jobs/agent_runs** (источник истины —
|
||||
liveness процесса). Это разные never-raise-домены и разные kill-switch'и; слияние
|
||||
в один тик смешало бы ответственности. Reaper копирует проверенный каркас
|
||||
`Reconciler`: `threading.Thread(daemon=True)` + `threading.Event`, старт/стоп в
|
||||
`main.lifespan`, снимок в `/queue`, per-job изоляция исключений.
|
||||
|
||||
**Liveness — трёхуровневая (defense in depth):**
|
||||
|
||||
1. **Tier-1 (liveness, основной): мёртвый pid.** Добавляем колонку `jobs.pid`
|
||||
(см. Р-4). В `_spawn` рядом с `run_id`/`started_at` пишем `proc.pid`. Reaper:
|
||||
`pid_alive(pid)` = `os.kill(pid, 0)` с обработкой `ProcessLookupError` (мёртв)
|
||||
/ `PermissionError` (жив, чужой) — единственный сигнал, ловящий «monitor умер
|
||||
ДО записи `finished_at`».
|
||||
2. **Tier-2 (completion race): exit_code записан, job ещё `running`.** Окно
|
||||
**неоднозначно**: это И «monitor умер между записью exit_code и
|
||||
`_finalize_job`», И «живой monitor ещё финализирует» — `_monitor_agent`
|
||||
пишет `exit_code` ПЕРВЫМ, затем git commit/push (+PR), БАГ-8-проверку и
|
||||
сетевые usage-комментарии в Plane (секунды-десятки секунд), и лишь потом
|
||||
`_try_advance_stage` → `_finalize_job`. pid агента к этому моменту уже мёртв в
|
||||
ОБОИХ случаях, поэтому по pid их не различить. **Анти-ложноположительность
|
||||
Tier-2 (FR-1.3, AC-3): finalization-grace.** Job реапится по Tier-2 только
|
||||
когда `exit_code` записан не меньше `reaper_finalize_grace_s` назад (потолок
|
||||
заведомо > максимального окна финализации). В пределах grace строка не
|
||||
трогается (живой финализирующий monitor НИКОГДА не реапится; нет дубль-advance
|
||||
/ дубль-enqueue). После grace monitor заведомо мёртв → исход **известен**.
|
||||
3. **Tier-3 (backstop по потолку):** job висит `running` дольше
|
||||
`reaper_max_running_s` (заведомо > max `agent_timeout`+grace). Реап даже когда
|
||||
liveness определить нельзя (pid переиспользован/неизвестен).
|
||||
|
||||
**Анти-ложноположительность (FR-1.3, AC-3):** по Tier-1 job реапится только после
|
||||
`reaper_dead_ticks` (≥2) ПОДРЯД тиков мёртвого pid — in-memory streak-счётчик по
|
||||
`job_id` (best-effort, сбрасывается на рестарте — но рестарт покрыт стартовым
|
||||
`requeue_running_jobs`). Tier-3 — одношаговый (порог времени, streak не нужен).
|
||||
Живой агент в пределах своего `agent_timeout` НЕ реапится никогда (pid жив + не
|
||||
превышен потолок).
|
||||
|
||||
**Действие при подтверждённой смерти (FR-1.2, AC-4) — переиспользование
|
||||
существующих контрактов, без дублирования:**
|
||||
|
||||
- **Атомарный reap-claim.** Перед любым действием с побочными эффектами reaper
|
||||
атомарно «застолбляет» строку тем же приёмом, что `claim_next_job`: терминальный
|
||||
flip несёт guard `WHERE id=? AND status='running'` и проверяет `rowcount`. При
|
||||
гонке (поздно доехавший monitor, стартовый `requeue_running_jobs`) проигравший
|
||||
видит `rowcount==0` и НЕ обрабатывает строку повторно (AC-5).
|
||||
- **Исход известен (Tier-2, exit_code в `agent_runs`, grace прошёл):**
|
||||
- `exit==0`: **claim-BEFORE-act, gate-driven idempotent advance.** Порядок
|
||||
критичен (см. «Атомарный reap-claim» выше): атомарный claim ОБЯЗАН
|
||||
предшествовать любому `advance_stage`/`enqueue_job`. Поскольку claim
|
||||
переводит строку ИЗ `running`, прогнать advance ДО claim, чтобы узнать цвет
|
||||
гейта, нельзя — поэтому канонический QG оценивается **read-only, без
|
||||
побочных эффектов** (тот же `_run_qg`, что у reconciler) ПЕРЕД claim:
|
||||
- стадия уже продвинута мимо этого агента → атомарный `done` без advance
|
||||
(идемпотентная уборка);
|
||||
- гейт зелёный → атомарный claim `done` ПЕРВЫМ, и только победитель claim
|
||||
выполняет `_try_advance_stage` (advance + `enqueue_job` следующей стадии)
|
||||
РОВНО один раз; проигравший claim (поздний monitor / стартовый
|
||||
`requeue_running_jobs`) НЕ делает побочных эффектов (нет дубль-advance /
|
||||
дубль-enqueue);
|
||||
- гейт красный (monitor умер ДО git-push, артефакта нет) → НЕ выдумываем
|
||||
`done`, уходим в ветку «исход неуспешен» ниже.
|
||||
**Источник истины — гейт, не «exit0».**
|
||||
- `exit!=0`: ровно существующий контракт `_finalize_job` (классификация
|
||||
transient/permanent, `attempts<max` → `queued`, иначе `failed`+Telegram).
|
||||
- **Исход неизвестен (Tier-1 мёртвый pid без exit_code, или Tier-3 backstop):**
|
||||
не выдумываем `exit0`. Если `attempts < max_attempts` → `queued` (как
|
||||
`requeue_running_jobs`); если бюджет исчерпан → `failed` + Telegram-алерт.
|
||||
|
||||
**Restart-safe (FR-1.5, AC-5):** reaper стартует в `lifespan` ПОСЛЕ
|
||||
`requeue_running_jobs()`, поэтому при рестарте сначала отрабатывает стартовый
|
||||
requeue, а периодический reaper лишь добивает то, что возникнет позже; атомарный
|
||||
guard `status='running'` исключает двойную обработку.
|
||||
|
||||
### Р-2. Проактивный реклейм stale/dead merge-lease — функции в `merge_gate.py`
|
||||
|
||||
Логика lease живёт в одном месте (`merge_gate.py`). Добавляем:
|
||||
- `pid_alive(pid) -> bool` (never-raise; ошибка/`PermissionError` → считаем
|
||||
«жив», т.е. консервативно НЕ реклеймим — безопаснее не трогать).
|
||||
- `reclaim_stale_lease(repo) -> bool` — для репо из области (см. ниже): прочитать
|
||||
lease; освободить (`release_merge_lease(repo, branch=holder_branch)` —
|
||||
holder-aware), если держатель **мёртв** (`pid` из lease не жив) ИЛИ **просрочен**
|
||||
(`age >= merge_lock_timeout_s`). Живой держатель в пределах TTL — НЕ трогать
|
||||
(AC-8, защита легитимного merge). Каждый реклейм → `logger.warning` +
|
||||
Telegram.
|
||||
|
||||
**Точки вызова (FR-2.1):**
|
||||
- на старте — в `lifespan` рядом с `requeue_running_jobs()`;
|
||||
- периодически — из тика reaper (один общий фоновый поток на оба механизма A и B).
|
||||
|
||||
**Условность (FR-2.4, AC-9):** реально только для `merge_gate_repos`/self-hosting
|
||||
(тот же предикат, что merge-gate); прочие репо — no-op. Kill-switch
|
||||
`lease_reclaim_enabled` (=false → остаётся лишь прежний ленивый TTL-реклейм в
|
||||
`acquire_merge_lease`). Контракт **never-raise**: ошибка реклейма логируется и не
|
||||
валит поток.
|
||||
|
||||
**pid-семантика lease:** lease пишет pid процесса ОРКЕСТРАТОРА (`os.getpid()`).
|
||||
После рестарта контейнера старый pid мёртв → детектируется. Риск pid-reuse
|
||||
(контейнер мог переиспользовать номер pid) закрыт тем, что реклейм срабатывает по
|
||||
**ИЛИ** (pid мёртв **ИЛИ** TTL истёк): даже при ложном «жив» TTL добьёт lease
|
||||
(контракт ORCH-043 сохранён). См. 10-tech-risks.
|
||||
|
||||
### Р-3. Идемпотентная финализация merge (Проблема C) — re-drive + guard, без новой merge-логики
|
||||
|
||||
Per FR-3.3 — НЕ создаём дублирующую merge-логику. Восстановление обеспечивается
|
||||
**штатными путями**:
|
||||
- reaper доводит зомби-job до `queued` → стадия `deploy-staging`/`deploy`
|
||||
переисполняется и снова проходит `check_branch_mergeable` (merge-gate), ЛИБО
|
||||
reconciler доигрывает переход по зелёному гейту;
|
||||
- дорогие шаги не повторяются «вхолостую»: `branch_is_behind_main == False` → этап
|
||||
rebase+re-test пропускается (ветка уже догнана);
|
||||
- lease при повторе берётся заново — `acquire_merge_lease` уже идемпотентен для
|
||||
«held by self» по branch (FR-3.4).
|
||||
|
||||
**Идемпотентность у самого merge (FR-3.2, AC-11):** добавляем детерминированный
|
||||
never-raise guard `pr_already_merged(repo, branch) -> bool` (переиспользует
|
||||
существующий Gitea-клиент; запрос состояния PR). Путь слияния (deployer/merge)
|
||||
консультируется с этим guard ПЕРЕД повторным merge: PR уже слит → no-op (без
|
||||
второго merge и без ошибки). Это единственная новая «merge-related» функция — она
|
||||
не сливает, а лишь читает состояние, поэтому не нарушает «no duplicate merge
|
||||
logic».
|
||||
|
||||
### Р-4. Изменение схемы БД — `jobs.pid INTEGER` (lightweight migration)
|
||||
|
||||
Колонка добавляется идемпотентно через существующий `_ensure_column(conn, "jobs",
|
||||
"pid", "INTEGER")` в `init_db` (паттерн уже применяется к `jobs.transient_attempts`
|
||||
/ `jobs.available_at` — безопасно на live prod DB). `pid` проставляется в `_spawn`
|
||||
рядом с `run_id`/`started_at`. **Альтернатива без миграции отвергнута** (см.
|
||||
Альтернативы): только по `agent_runs.finished_at/exit_code` нельзя поймать
|
||||
зомби, у которого monitor умер ДО записи exit_code — а это и есть основной класс
|
||||
инцидента. `STAGE_TRANSITIONS`, `QG_CHECKS`, схема `agent_runs`, файл-схема lease —
|
||||
без изменений.
|
||||
|
||||
### Р-5. Конфигурация (`src/config.py`, env `ORCH_*`)
|
||||
|
||||
| Настройка | Назначение | Дефолт |
|
||||
|-----------|-----------|--------|
|
||||
| `reaper_enabled` | глобальный kill-switch job-reaper | `True` |
|
||||
| `reaper_interval_s` | период сканирования | `60` |
|
||||
| `reaper_dead_ticks` | подряд тиков мёртвого pid перед реапом (Tier-1) | `2` |
|
||||
| `reaper_max_running_s` | потолок `running` (Tier-3 backstop), > max agent_timeout+grace | `3600` |
|
||||
| `reaper_finalize_grace_s` | Tier-2 grace: сколько `exit_code` должен быть записан до реапа (> max окна финализации) | `300` |
|
||||
| `lease_reclaim_enabled` | kill-switch проактивного реклейма lease | `True` |
|
||||
| (reuse) `merge_lock_timeout_s` | TTL lease | `300` |
|
||||
| (reuse) `merge_gate_repos` | область применения lease-reclaim | как есть |
|
||||
|
||||
`false` → строго прежнее поведение (AC-14).
|
||||
|
||||
### Р-6. Наблюдаемость (`GET /queue`)
|
||||
|
||||
Блок `reaper` (образец `reconcile`/`post_deploy`): `enabled`, `interval`,
|
||||
`last_run_ts`, `reaped_total`, `last_reaped` (`{job_id, agent, outcome}`),
|
||||
`lease_reclaimed_total`. Каждый reap и lease-reclaim → `logger.warning` с
|
||||
идентификаторами (`job_id`, `run_id`, `pid`, `repo`, `branch`). reap→`failed` и
|
||||
lease-reclaim → Telegram (AC-16).
|
||||
|
||||
### Р-7. Lifespan (`src/main.py`)
|
||||
|
||||
Старт (после существующего `requeue_running_jobs()`):
|
||||
```
|
||||
init_db() # + _ensure_column(jobs, pid)
|
||||
... orphan-recovery (M-1) ...
|
||||
requeue_running_jobs()
|
||||
+ startup lease-reclaim # reclaim_stale_lease для merge_gate_repos
|
||||
worker.start()
|
||||
reconciler.start()
|
||||
+ reaper.start() # НОВЫЙ daemon-поток (A + периодический B)
|
||||
```
|
||||
Стоп (reverse): `reaper.stop()` → `reconciler.stop()` → `worker.stop()`.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Reaper как часть reconciler** — отвергнуто: смешивает stage-уровень и
|
||||
jobs-уровень, два разных kill-switch'а в одном тике, хуже изоляция отказов.
|
||||
- **Без `jobs.pid`, только эвристика `agent_runs` + потолок** — отвергнуто как
|
||||
основной механизм: не ловит зомби, чей monitor умер ДО записи `exit_code`
|
||||
(главный класс инцидента). Эвристика оставлена как Tier-2/Tier-3 поверх pid.
|
||||
- **БД-lock вместо файлового lease / внешний брокер очередей** — вне объёма
|
||||
(BRD §4), несоразмерно для single-node SQLite.
|
||||
- **Реаниматор фабрикует `exit0` и форсит `done`** — отвергнуто: ложный `done`
|
||||
без реальной работы (если git-push не случился). Выбран gate-driven advance:
|
||||
источник истины — канонический QG, не предположение об успехе.
|
||||
- **Новый статус `reaping` в enum `jobs.status`** — отвергнуто: меняет контракт
|
||||
статусов; атомарного guard `WHERE status='running'` достаточно.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы:**
|
||||
- Зомби-job самовосстанавливается БЕЗ рестарта процесса → очередь не встаёт
|
||||
(групповой риск снят для всех проектов общего инстанса).
|
||||
- Залипший lease освобождается проактивно (старт + период), не дожидаясь TTL и
|
||||
чужого acquire.
|
||||
- Незавершённый merge докатывается штатным путём, идемпотентно; ручные шаги
|
||||
оператора устранены → снят технический блокер ORCH-54.
|
||||
- Контракты неизменны (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука); один новый столбец через проверенный idempotent-паттерн.
|
||||
|
||||
**Минусы / ограничения:**
|
||||
- pid-liveness валиден в предположении ОДНОГО pid-namespace (агент — дочерний
|
||||
процесс оркестратора в том же контейнере). Multi-container/namespaced pid →
|
||||
pid-liveness ненадёжен; закрыто backstop'ом по времени и TTL. См. 10-tech-risks.
|
||||
- streak-счётчик in-memory best-effort: после рестарта он сбрасывается, но
|
||||
стартовый `requeue_running_jobs` покрывает рестарт-сценарий.
|
||||
- Tier-3 backstop консервативен (потолок > max timeout); очень долгий легитимный
|
||||
агент (близко к потолку) теоретически может быть реапнут — потолок выбран с
|
||||
большим запасом, чтобы этого не случалось (AC-3).
|
||||
|
||||
## Инварианты (НЕ нарушать)
|
||||
- Reaper/lease-reclaim НЕ рестартят/не роняют прод-контейнер `orchestrator` и НЕ
|
||||
инициируют git-push в `main` (AC-12). Реклейм lease — только удаление
|
||||
файла-lease, без git-операций.
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, сигнатуры/поведение `check_*` (в т.ч.
|
||||
`check_branch_mergeable`), БАГ-8 откат, exit-коды deploy-хука — без изменений;
|
||||
новых QG checks/стадий нет (AC-13).
|
||||
- never-raise на единицу фоновой работы; идемпотентность (атомарный guard +
|
||||
gate-driven advance); restart-safe; тишина при отсутствии аномалий.
|
||||
- Анти-ложноположительность (FR-1.3): живой долгий агент не реапится.
|
||||
|
||||
## Связи
|
||||
- Базируется на: ORCH-1 (очередь, adr-0002), ORCH-043 (merge-gate, adr-0006),
|
||||
ORCH-053 (reconciler-паттерн, adr-0007), ORCH-36 (self-deploy, adr-0007).
|
||||
- Сквозной ADR: adr-0011.
|
||||
- Разблокирует: ORCH-54 (полностью автономный self-deploy).
|
||||
42
docs/work-items/ORCH-065/07-infra-requirements.md
Normal file
42
docs/work-items/ORCH-065/07-infra-requirements.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 07 — Требования к инфраструктуре (ORCH-065)
|
||||
|
||||
## Топология
|
||||
**Без изменений.** Новых контейнеров, портов, сетевых сервисов, внешних
|
||||
зависимостей нет. Job-reaper — ещё один фоновый daemon-поток ВНУТРИ существующего
|
||||
процесса оркестратора (как `queue_worker` и `reconciler`), стартует/останавливается
|
||||
в `main.lifespan`. Деплой/рестарт прод-контейнера в рамках задачи НЕ требуется и
|
||||
ЗАПРЕЩЁН (self-hosting safety) — выкатка через штатный `deploy-staging → deploy`.
|
||||
|
||||
## Допущение pid-namespace (важно для liveness-детекции)
|
||||
- Агент запускается как `subprocess.Popen(["bash","-c",cmd])` — **дочерний
|
||||
процесс оркестратора в ТОМ ЖЕ pid-namespace** (один контейнер). Значит
|
||||
`os.kill(jobs.pid, 0)` корректно отражает liveness агента, пока жив сам
|
||||
оркестратор. Это инвариант текущей упаковки (один контейнер на инстанс).
|
||||
- Lease пишет `pid = os.getpid()` — pid ПРОЦЕССА ОРКЕСТРАТОРА. После рестарта
|
||||
контейнера старый pid мёртв → детектируется. Риск переиспользования номера pid
|
||||
новым процессом закрыт условием «pid мёртв **ИЛИ** TTL истёк»: TTL добивает
|
||||
lease в любом случае (контракт ORCH-043 сохранён).
|
||||
- **Если в будущем агенты переедут в отдельные контейнеры/namespace** — Tier-1
|
||||
pid-liveness станет ненадёжной; тогда полагаемся на Tier-2 (exit_code) и Tier-3
|
||||
(потолок `reaper_max_running_s`). Зафиксировано в 10-tech-risks.
|
||||
|
||||
## Поведение при self-restart (ORCH-36 executable self-deploy)
|
||||
Self-restart прод-контейнера во время `deploy` — ровно тот сценарий, что плодит
|
||||
зомби: monitor-поток умирает вместе со старым контейнером. После рестарта:
|
||||
1. стартовый `requeue_running_jobs()` + стартовый `reclaim_stale_lease` чистят
|
||||
состояние, оставшееся от убитого процесса;
|
||||
2. периодический reaper добивает то, что возникнет позже без рестарта.
|
||||
Reaper/lease-reclaim сами НИКОГДА не рестартят и не роняют прод-контейнер и не
|
||||
делают git-push в `main` (AC-12).
|
||||
|
||||
## Эксплуатационные ручки (env, хост `.env`/`.env.staging`)
|
||||
`ORCH_REAPER_ENABLED`, `ORCH_REAPER_INTERVAL_S`, `ORCH_REAPER_DEAD_TICKS`,
|
||||
`ORCH_REAPER_MAX_RUNNING_S`, `ORCH_LEASE_RECLAIM_ENABLED`; переиспользуются
|
||||
`ORCH_MERGE_LOCK_TIMEOUT_S`, `ORCH_MERGE_GATE_REPOS`. Все флаги документируются в
|
||||
`.env.example` (developer-стадия). Полное отключение (`false`) → строго прежнее
|
||||
поведение.
|
||||
|
||||
## Документация эксплуатации
|
||||
`docs/operations/INFRA.md` — добавить (best-effort, developer/PR) короткое
|
||||
упоминание поведения reaper/lease-reclaim при self-restart. Топологическая карта
|
||||
INFRA.md не меняется.
|
||||
29
docs/work-items/ORCH-065/08-data-requirements.md
Normal file
29
docs/work-items/ORCH-065/08-data-requirements.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 08 — Требования к данным (ORCH-065)
|
||||
|
||||
## Изменение схемы: `jobs.pid`
|
||||
|
||||
| Поле | Значение |
|
||||
|------|----------|
|
||||
| Таблица | `jobs` |
|
||||
| Колонка | `pid` |
|
||||
| Тип | `INTEGER` (nullable, без DEFAULT) |
|
||||
| Назначение | pid агентского процесса (`subprocess.Popen.pid` из `launcher._spawn`) для liveness-детекции зомби job-reaper'ом (Tier-1) |
|
||||
| Механизм миграции | `_ensure_column(conn, "jobs", "pid", "INTEGER")` в `db.init_db` — идемпотентно, no-op если колонка уже есть |
|
||||
| Безопасность на live prod DB | ДА. Тот же паттерn уже применён к `jobs.transient_attempts`, `jobs.available_at`, `events.delivery_id`, `agent_runs.*`. `ALTER TABLE ADD COLUMN` в SQLite — мгновенная метаданная-операция, не блокирует и не переписывает строки |
|
||||
| Заполнение | в `_spawn` рядом с существующим `UPDATE jobs SET run_id=?, started_at=datetime('now') WHERE id=?` добавить `pid=?` (`proc.pid`). Старые строки остаются `pid IS NULL` → для них Tier-1 неприменим, работают Tier-2/Tier-3 |
|
||||
|
||||
## Что НЕ меняется
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS` — без изменений (это контракты).
|
||||
- Схема `agent_runs` — без изменений (`finished_at`/`exit_code` уже есть — основа Tier-2).
|
||||
- Файл-схема merge-lease `.merge-lease-<repo>.json` — без изменений (`pid`,
|
||||
`acquired_at`, `branch`, `work_item_id`, `task_id` уже пишутся
|
||||
`acquire_merge_lease`).
|
||||
- `jobs.status` enum (`queued|running|done|failed`) — без изменений; новый статус
|
||||
`reaping` НЕ вводится (атомарного guard `WHERE status='running'` достаточно).
|
||||
|
||||
## Совместимость / откат
|
||||
- Откат миграции не требуется: лишняя nullable-колонка безвредна при
|
||||
`reaper_enabled=false`.
|
||||
- `pid IS NULL` (строки до миграции, или если запись pid не успела) → reaper не
|
||||
делает Tier-1, опирается на Tier-2 (exit_code) и Tier-3 (потолок). Поведение
|
||||
деградирует gracefully, ложноположительных реапов не возникает.
|
||||
22
docs/work-items/ORCH-065/10-tech-risks.md
Normal file
22
docs/work-items/ORCH-065/10-tech-risks.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 10 — Технические риски (ORCH-065)
|
||||
|
||||
| # | Риск | Вероятн. | Влияние | Митигация |
|
||||
|---|------|----------|---------|-----------|
|
||||
| R-1 | **Ложноположительный реап живого долгого агента** (AC-3). Reaper помечает зомби работающий агент → потеря работы, дубль-запуск. | Сред. | Высокое | Tier-1 требует `reaper_dead_ticks`(≥2) подряд тиков мёртвого pid; живой pid = `os.kill(pid,0)` без `ProcessLookupError`. Tier-3 потолок `reaper_max_running_s` выбирается заведомо > max `agent_timeout`+grace. Юнит-тест TC-02/TC-03. |
|
||||
| R-2 | **Ложный `done` без выполненной работы.** Reaper при exit0-зомби помечает job done, хотя git-push/advance не случились (monitor умер до них). | Сред. | Высокое | Реап exit0 НЕ форсит done напрямую — идёт через **gate-driven** `_try_advance_stage`: канонический QG проверяет наличие артефакта/PR; нет артефакта → красный гейт → НЕ advance → ветка «исход неуспешен» (requeue). Источник истины — гейт, не «exit0». |
|
||||
| R-3 | **pid-reuse / namespace.** Номер pid переиспользован новым процессом → ложное «жив» (lease не реклеймится; зомби-job не реапится по Tier-1). | Низк. | Сред. | Lease: условие «pid мёртв **ИЛИ** TTL истёк» — TTL добивает в любом случае. Job-reaper: Tier-3 backstop по времени ловит то, что Tier-1 пропустил. Допущение «один pid-namespace» зафиксировано в 07-infra. |
|
||||
| R-4 | **Гонка reaper vs поздно доехавший monitor / стартовый `requeue_running_jobs`** → двойная обработка строки. | Сред. | Сред. | Атомарный reap-claim `UPDATE ... WHERE id=? AND status='running'` + проверка `rowcount` (образец `claim_next_job`). Reaper стартует ПОСЛЕ `requeue_running_jobs` в lifespan. Юнит-тест TC-06. |
|
||||
| R-5 | **Реклейм живого lease** → параллельный конфликтный merge, риск красного `main` self-hosting. | Низк. | Высокое | `reclaim_stale_lease` освобождает ТОЛЬКО при «держатель мёртв ИЛИ TTL истёк»; живой держатель в пределах TTL не трогается. holder-aware `release_merge_lease(repo, branch)`. Юнит-тест TC-12. |
|
||||
| R-6 | **Реклейм инициирует git-операцию / трогает прод-контейнер** (нарушение self-hosting safety, AC-12). | Низк. | Высокое | Реклейм = только удаление файла-lease (`os.remove`), без git. Reaper не вызывает деплой-хук/рестарт. Явный инвариант в ADR + тест/ревью. |
|
||||
| R-7 | **Идемпотентность merge не достигнута**: повторный проход стадии делает второй merge уже слитого PR. | Сред. | Сред. | never-raise guard `pr_already_merged(repo,branch)` (читает состояние PR) консультируется перед merge → уже слит = no-op. `branch_is_behind_main==False` пропускает rebase+re-test. Юнит-тест TC-16, интеграция TC-17. |
|
||||
| R-8 | **streak-счётчик in-memory теряется при рестарте** → задержка реапа или сброс прогресса. | Низк. | Низкое | Рестарт-сценарий покрыт стартовым `requeue_running_jobs` (мгновенно чистит running). Периодический reaper нужен лишь для зомби БЕЗ рестарта; сброс счётчика лишь переоткладывает реап на `reaper_dead_ticks` тиков. |
|
||||
| R-9 | **never-raise нарушен** — необработанное исключение валит daemon-поток reaper → защита тихо отключается. | Низк. | Сред. | Per-job изоляция `try/except` (образец `reconciler.reconcile_gate_once`) + внешний `try/except` в `_run`. Юнит-тест TC-08/TC-14. |
|
||||
| R-10 | **Регресс существующих тестов** merge_gate/queue/reconciler/deploy. | Низк. | Сред. | Контракты неизменны (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/exit-коды хука); только новая колонка + новый поток + флаги (дефолт сохраняет поведение). Полный прогон `pytest tests/ -q` (regression в 04-test-plan). |
|
||||
|
||||
## Открытые вопросы / follow-up
|
||||
- **Полная автоматизация merge-финализации.** Если деплой-merge (deployer/ORCH-36
|
||||
detached host-process) окажется не полностью идемпотентным к повторному проходу,
|
||||
может понадобиться доп. работа поверх `pr_already_merged`. Здесь закрываем
|
||||
технический блокер; полный авто-approve деплоя — ORCH-54.
|
||||
- Допущение «агенты — дочерние процессы в одном pid-namespace» (R-3) должно быть
|
||||
пересмотрено, если упаковка агентов изменится (отдельные контейнеры).
|
||||
70
docs/work-items/ORCH-065/12-review.md
Normal file
70
docs/work-items/ORCH-065/12-review.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-065
|
||||
verdict: APPROVED
|
||||
version: 3
|
||||
---
|
||||
|
||||
# Review ORCH-065
|
||||
|
||||
## Summary
|
||||
|
||||
Задача закрывает три связанных класса отказов «процесс/поток умер, а ресурс остался
|
||||
захваченным навсегда»: zombie jobs (A), залипший merge-lease (B), неидемпотентная
|
||||
финализация merge (C). Реализация качественная: новый daemon-поток `src/job_reaper.py`
|
||||
по образцу `reconciler` (never-raise, kill-switch, снимок в `/queue`), трёхуровневая
|
||||
liveness, атомарный `reap_running_job(... WHERE status='running')`, проактивный реклейм
|
||||
lease (`pid_alive` + `reclaim_stale_lease`), идемпотентный guard `pr_already_merged`,
|
||||
колонка `jobs.pid` через идемпотентный `_ensure_column`.
|
||||
|
||||
**Все блокеры предыдущих ревью устранены:**
|
||||
- v1 P0 (guard `pr_already_merged` не подключён к merge-пути) — устранён `aa46e5d`:
|
||||
промпт `.openclaw/agents/deployer.md` консультирует `pr_already_merged` ПЕРЕД любым
|
||||
(повторным) merge (AC-11 wiring на месте, подтверждено строками 94–105/152).
|
||||
- v2 P1 (Tier-2 реапит живой финализирующий monitor; side-effects ДО атомарного claim,
|
||||
нарушение ADR-001 Р-1) — устранён `3e2eb27` двумя мерами:
|
||||
1. **Tier-2 finalization grace** — новая колонка `finished_age_s` в `get_running_jobs`
|
||||
(`src/db.py:609`) + настройка `reaper_finalize_grace_s` (дефолт 300с); Tier-2
|
||||
реапит только при `finished_age >= grace`, иначе строка не трогается
|
||||
(`src/job_reaper.py:197-209`). Живой финализирующий monitor больше не реапится
|
||||
(FR-1.3/AC-3).
|
||||
2. **claim-before-act** — `_reap_exit0` (`src/job_reaper.py:242-286`) сначала оценивает
|
||||
канонический QG read-only (`_gate_is_green` → `_run_qg`, без побочных эффектов),
|
||||
затем атомарно claim `done` ПЕРВЫМ, и только победитель claim выполняет
|
||||
`_gate_driven_advance`. Проигравший гонку (поздний monitor / стартовый requeue) не
|
||||
делает НИКАКИХ побочных эффектов → нет дубль-advance/дубль-enqueue (FR-1.2/AC-4).
|
||||
- v2 P3 (битая ссылка на adr-0011 в CHANGELOG) — исправлена в `3e2eb27`
|
||||
(`adr-0011-job-reaper-lease-reclaim.md`).
|
||||
|
||||
Инварианты сохранены (AC-13): ORCH-065-коммиты (`1a2e881`/`aa46e5d`/`3e2eb27`) НЕ касаются
|
||||
`src/stages.py` и `src/qg/checks.py` — `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/БАГ-8/
|
||||
exit-коды хука не тронуты; реклейм lease — только удаление файла, без git-операций
|
||||
(AC-12). Документация (README, internals, ADR-001, глобальный adr-0011, CHANGELOG,
|
||||
.env.example) обновлена в этом же PR (AC-17). Новые тесты покрывают grace-окно,
|
||||
lost-claim-no-side-effects, already-advanced-идемпотентность. `pytest tests/ -q` —
|
||||
**747 passed**.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice to have
|
||||
- нет
|
||||
|
||||
## Документация
|
||||
|
||||
Обновлена корректно и в этом же PR (AC-17 PASS): `docs/architecture/README.md`
|
||||
(раздел про job-reaper + lease-reclaim, таблицы БД и `/queue`),
|
||||
`docs/architecture/internals.md`, `docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md`
|
||||
(+ запись в `adr/README.md`),
|
||||
`docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md`, `CHANGELOG.md`
|
||||
(ссылка на adr-0011 исправлена), `.env.example` (флаги `ORCH_REAPER_*` /
|
||||
`ORCH_REAPER_FINALIZE_GRACE_S` / `ORCH_LEASE_RECLAIM_ENABLED`). ADR-001 Р-1 и реализация
|
||||
exit0-пути теперь согласованы (claim-before-act).
|
||||
92
docs/work-items/ORCH-065/13-test-report.md
Normal file
92
docs/work-items/ORCH-065/13-test-report.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-065
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-065
|
||||
|
||||
Тема: job-reaper + проактивный реклейм stale/dead merge-lease + идемпотентная
|
||||
финализация merge. Прогон полного регресса в ветке
|
||||
`feature/ORCH-065-bug-zombie-jobs-merge-lease-ru`. Review-вердикт — APPROVED (v3).
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Ветка: feature/ORCH-065-bug-zombie-jobs-merge-lease-ru (worktree)
|
||||
- Прод (8500): health `200 {"status":"ok"}` — НЕ перезапускался (self-hosting инвариант соблюдён)
|
||||
- Дата: 2026-06-07
|
||||
|
||||
## Smoke API (прод 8500, read-only)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | 200 `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | 200, активные задачи отдаются (ORCH-065 в `testing`, ET-013 в `development`) |
|
||||
| `GET /queue` | 200, counts/resilience/reconcile/post_deploy присутствуют |
|
||||
|
||||
Примечание: блок `reaper` в `/queue` прода (8500) ОТСУТСТВУЕТ — ожидаемо, т.к. прод
|
||||
исполняет ещё не задеплоенный (до-ORCH-065) код. Контракт блока `reaper` проверен
|
||||
тестом TC-18 (`tests/test_queue.py::test_tc18_queue_endpoint_has_reaper_block`)
|
||||
против кода ветки — PASS. Curl недоступен в окружении, smoke выполнен через
|
||||
`urllib.request` (read-only, без побочных эффектов на прод).
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Тип | Модуль | Покрывает | Результат |
|
||||
|-------|-----|--------|-----------|-----------|
|
||||
| TC-01 | unit | test_job_reaper.py | AC-1 (реап мёртвого job без рестарта) | PASS |
|
||||
| TC-02 | unit | test_job_reaper.py | AC-3 (живой агент не реапится) | PASS |
|
||||
| TC-03 | unit | test_job_reaper.py | FR-1.3 (устойчивость reaper_dead_ticks) | PASS |
|
||||
| TC-04 | unit | test_job_reaper.py | FR-1.1/AC-1 (backstop reaper_max_running_s) | PASS |
|
||||
| TC-05 | unit | test_job_reaper.py | AC-4 (исход по результату: done/queued/failed) | PASS |
|
||||
| TC-06 | unit | test_job_reaper.py | AC-5 (атомарность reap-UPDATE guard) | PASS |
|
||||
| TC-07 | unit | test_job_reaper.py | AC-14 (kill-switch reaper_enabled=false) | PASS |
|
||||
| TC-08 | unit | test_job_reaper.py | AC-9 (never-raise per-job) | PASS |
|
||||
| TC-09 | integration | test_queue.py | AC-2 (разблокировка очереди concurrency=1) | PASS |
|
||||
| TC-10 | unit | test_merge_lease_reclaim.py | AC-6 (реклейм lease мёртвого pid) | PASS |
|
||||
| TC-11 | unit | test_merge_lease_reclaim.py | AC-7 (реклейм по TTL сохранён) | PASS |
|
||||
| TC-12 | unit | test_merge_lease_reclaim.py | AC-8 (живой lease не трогается) | PASS |
|
||||
| TC-13 | unit | test_merge_lease_reclaim.py | AC-9 (условность self-hosting/no-op) | PASS |
|
||||
| TC-14 | unit | test_merge_lease_reclaim.py | AC-9 (never-raise при ошибке lease-файла) | PASS |
|
||||
| TC-15 | unit | test_merge_lease_reclaim.py | AC-14 (kill-switch lease_reclaim_enabled=false) | PASS |
|
||||
| TC-16 | unit | test_merge_gate.py | AC-11 (идемпотентность при уже слитом PR) | PASS |
|
||||
| TC-17 | integration | test_merge_gate_race.py | AC-10 (докатывание незавершённого merge) | PASS |
|
||||
| TC-18 | integration | test_queue.py | AC-15 (блок reaper в /queue) | PASS |
|
||||
| TC-19 | unit | test_config.py | AC-13 (контракты STAGE_TRANSITIONS/QG_CHECKS неизменны) | PASS |
|
||||
| TC-20 | unit | test_config.py | §5/AC-14 (новые настройки reaper_*/lease_reclaim_*) | PASS |
|
||||
| TC-21 | unit | test_job_reaper.py | FR-2.1/AC-6 (стартовый реклейм в lifespan) | PASS |
|
||||
|
||||
Все 21 TC из плана — PASS.
|
||||
|
||||
## Сопоставление с критериями приёмки (03-acceptance-criteria.md)
|
||||
- A (AC-1…AC-5): job-reaper — покрыты TC-01..TC-06, TC-09 → PASS
|
||||
- B (AC-6…AC-9): lease-reclaim — покрыты TC-10..TC-15 → PASS
|
||||
- C (AC-10, AC-11): идемпотентная финализация — TC-16, TC-17 → PASS
|
||||
- D (AC-12 прод не трогается, AC-13 контракты, AC-14 kill-switches): TC-07, TC-15, TC-19, TC-20 + smoke прода без рестарта → PASS
|
||||
- E (AC-15 /queue, AC-16 логи/алерты): TC-18 → PASS
|
||||
- F (AC-17 документация): review подтвердил обновление README/internals/ADR-001/adr-0011/CHANGELOG/.env.example (APPROVED) → PASS
|
||||
- G (AC-18 регресс зелёный): `pytest tests/` 747 passed → PASS
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
### Целевые модули плана
|
||||
```
|
||||
$ python -m pytest tests/test_job_reaper.py tests/test_merge_lease_reclaim.py \
|
||||
tests/test_merge_gate.py tests/test_merge_gate_race.py \
|
||||
tests/test_queue.py tests/test_config.py -q
|
||||
92 passed, 1 warning in 3.40s
|
||||
```
|
||||
|
||||
### Полный регресс
|
||||
```
|
||||
$ python -m pytest tests/ -v --tb=short
|
||||
======================= 747 passed, 1 warning in 15.47s ========================
|
||||
```
|
||||
(1 warning — PydanticDeprecatedSince20 в src/config.py, не связан с ORCH-065,
|
||||
предсуществующий.)
|
||||
|
||||
## Итог
|
||||
**PASS.** Полный регресс — 747 passed, 0 failed. Все 21 TC тест-плана зелёные,
|
||||
все критерии приёмки (AC-1…AC-18) подтверждены. Smoke прода — health/status/queue
|
||||
200 OK, прод-контейнер не перезапускался (self-hosting инвариант соблюдён).
|
||||
Задача готова к переходу на стадию `deploy-staging`.
|
||||
32
docs/work-items/ORCH-065/15-staging-log.md
Normal file
32
docs/work-items/ORCH-065/15-staging-log.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T16:13:48Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live staging environment (8501),
|
||||
run canonically inside the `orchestrator-staging` container (ORCH-048, ADR-001).
|
||||
|
||||
**Verdict:** SUCCESS — `staging_check.py` exited **0**. All REAL (pipeline)
|
||||
checks green; the only failures are the two known sandbox-infra checks
|
||||
(C9a/C9b), which are waived per ORCH-061 because every REAL check passed.
|
||||
|
||||
## Result
|
||||
|
||||
- RESULT: 8/10 checks PASS
|
||||
- REAL failed: none
|
||||
- SANDBOX_INFRA failed (waived): C9a Branch appears in orchestrator-sandbox; C9b Analyst job enqueued in staging queue
|
||||
|
||||
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
|
||||
|
||||
## Block detail
|
||||
|
||||
- [Block A] SMOKE — A1 /health, A2 /queue, A3 ORCH_STAGING=true → PASS
|
||||
- [Block B] ACCESS — B4 Plane sandbox, B5 Gitea orchestrator-sandbox (push=true), B6 registry isolation (sandbox present, prod ET/ORCH absent) → PASS
|
||||
- [Block C] E2E (stub) — C7 create issue in SANDBOX, C8 trigger pipeline via /webhook/plane → PASS; C9a/C9b → FAIL (sandbox-infra, waived)
|
||||
- CLEANUP — Plane issue deleted (HTTP 204)
|
||||
|
||||
tolerance: staging_infra_tolerance_enabled=True
|
||||
7
docs/work-items/ORCH-066/00-business-request.md
Normal file
7
docs/work-items/ORCH-066/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: [высокий] Статусная модель Plane: осмысленные статусы этапов
|
||||
|
||||
Work Item ID: ORCH-066
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
110
docs/work-items/ORCH-066/01-brd.md
Normal file
110
docs/work-items/ORCH-066/01-brd.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 01 — Business Requirements Document (BRD)
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Заголовок:** [высокий] Статусная модель Plane: осмысленные статусы этапов
|
||||
**Стадия:** analysis
|
||||
**Автор:** Analyst
|
||||
**Дата:** 2026-06-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
Статусная модель Plane оркестратора имеет **семантические перегрузки**: один и тот
|
||||
же Plane-статус используется для несовместимых смыслов, из-за чего:
|
||||
|
||||
- оператор не понимает, на каком реально этапе стоит задача (доска нечитаема);
|
||||
- повышается риск ошибки оператора (например, неверный ручной перевод статуса);
|
||||
- `In Progress` одновременно означает «человек запускает конвейер», «идёт анализ»,
|
||||
«идёт прод-деплой» и «возврат из Needs Input» — четыре разных смысла на одном статусе.
|
||||
|
||||
Уже частично исправлено: ORCH-059 ввёл отдельный статус для подтверждения деплоя
|
||||
(`Confirm Deploy`), разгрузив перегруженный `Approved`. ORCH-066 завершает наведение
|
||||
порядка по **утверждённой Owner** статусной модели.
|
||||
|
||||
### Два слоя (критично различать)
|
||||
|
||||
| Слой | Что это | Источник | Трогаем? |
|
||||
|------|---------|----------|----------|
|
||||
| **A** | `STAGE_TRANSITIONS` — внутренняя машина стадий (`created→analysis→…→done`) | `src/stages.py` | **НЕТ (инвариант)** |
|
||||
| **B** | Plane-статусы — индикация на доске | `src/plane_sync.py` + точки в `src/stage_engine.py` / `src/webhooks/plane.py` | **ДА** |
|
||||
|
||||
ORCH-066 меняет **только слой B** и точки, где код вручную проставляет Plane-статусы.
|
||||
|
||||
---
|
||||
|
||||
## 2. Целевая статусная модель (решение 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** — человек решил не делать задачу (валидный выход из In Review).
|
||||
|
||||
---
|
||||
|
||||
## 3. Бизнес-требования
|
||||
|
||||
| ID | Требование | Приоритет |
|
||||
|----|------------|-----------|
|
||||
| **BR-1** | Каждый этап конвейера показывается на доске Plane осмысленным статусом (To Analyse / Analysis / Code-Review / Awaiting Deploy / Deploying / Monitoring after Deploy). | Must |
|
||||
| **BR-2** | `To Analyse` — единый человеческий вход: (а) старт нового конвейера, (б) resume/relaunch аналитика при возврате из Needs Input. Заменяет роль `In Progress` как входа-триггера. | Must |
|
||||
| **BR-3** | Стадия `analysis` индицируется отдельным статусом `Analysis` (орк ставит при старте/relaunch аналитика), а не `In Progress`. | Must |
|
||||
| **BR-4** | Стадия `review` индицируется Plane-статусом `Code-Review` (переименование `Review`). | Must |
|
||||
| **BR-5** | Self-deploy Phase A (approval-pending) ставит `Awaiting Deploy` вместо `In Review`. | Must |
|
||||
| **BR-6** | Self-deploy Phase B (старт прод-деплоя) ставит `Deploying`. | Must |
|
||||
| **BR-7** | Self-deploy Phase C (health-OK финализация) ставит `Monitoring after Deploy` (НЕ `Done` сразу). | Must |
|
||||
| **BR-8** | Post-deploy monitor (ORCH-021): чистое закрытие окна (HEALTHY) → `Done`; UNHEALTHY/деградация → `Blocked`. | Must |
|
||||
| **BR-9** | `In Review` разгрузить: оставить ТОЛЬКО за approve-pending артефактов конвейера (BRD/ревью). Выходы: `Approved` (вперёд), `Rejected` (откат), `Cancelled` (человек отменил). | Must |
|
||||
| **BR-10** | `Needs Input` — БЕЗ ИЗМЕНЕНИЙ. Остаётся только у аналитика (`01-questions.md` → `set_issue_needs_input`). Механизм не трогать. | Must |
|
||||
| **BR-11** | Возврат аналитика из Needs Input выполняется через `To Analyse` (а НЕ через `In Progress`). Логика fork «старт vs resume» (по наличию task + active-job) сохраняется. | Must (грабли R1) |
|
||||
| **BR-12** | **Fail-closed:** отсутствие нового статуса в проекте (enduro / Plane API down / fallback `_DEFAULT_STATES`) НЕ приводит к падению; поведение остаётся backward-compatible (паттерн ORCH-059 AC-7). | Must |
|
||||
| **BR-13** | Reconciler не «оживляет» активные ожидания (`Awaiting Deploy` / `Deploying` / `Monitoring after Deploy`) как зависшие задачи (Guard 2 skip-list). | Must |
|
||||
| **BR-14** | Документация (golden source) обновлена в том же PR: `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`, ADR per-work-item. | Must |
|
||||
|
||||
---
|
||||
|
||||
## 4. Границы (Out of Scope / НЕ трогать)
|
||||
|
||||
- `STAGE_TRANSITIONS` (`src/stages.py`) — машина стадий, инвариант.
|
||||
- `QG_CHECKS`, `check_deploy_status`, exit-коды хука (0/1/2), merge-gate, схема БД.
|
||||
- `Confirm Deploy` (уже работает, ORCH-059).
|
||||
- Механизм `Needs Input` (analyst-only) — не расширять, не менять.
|
||||
- Поведение прод-деплоя **не-self** репозиториев (enduro-trails): для них терминальный
|
||||
переход остаётся `deploy → Done` как сейчас (Monitoring after Deploy не применяется —
|
||||
post-deploy monitor армится только для self-hosting).
|
||||
- Автоматический approve / авто-rollback self-hosting (ORCH-54 / ORCH-021 политика
|
||||
ALERT_ONLY) — не меняется.
|
||||
|
||||
---
|
||||
|
||||
## 5. Инфра-предусловие (вне кода, делает оператор)
|
||||
|
||||
Новые Plane-статусы в проекте **ORCH** создаёт оператор через Plane API **ДО** эксплуатации:
|
||||
`To Analyse`, `Analysis`, `Code-Review`, `Awaiting Deploy`, `Deploying`,
|
||||
`Monitoring after Deploy` (`Confirm Deploy` уже есть).
|
||||
|
||||
Резолвер (`_PLANE_NAME_TO_KEY` + `get_project_states`) подхватывает их **по имени** с
|
||||
**fail-closed fallback** на `_DEFAULT_STATES` (см. BR-12). Документируется в
|
||||
`07-infra-requirements.md` (создаёт архитектор) и в `docs/operations/`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
- Plane показывает осмысленные статусы на каждом этапе.
|
||||
- Возврат аналитика из Needs Input работает через `To Analyse`.
|
||||
- Phase A → `Awaiting Deploy`, Phase B → `Deploying`, Phase C → `Monitoring after Deploy`,
|
||||
окно HEALTHY → `Done`, фейл → `Blocked`.
|
||||
- `STAGE_TRANSITIONS` не изменён.
|
||||
- `pytest tests/ -q` — зелёный. Fail-closed покрыт тестами.
|
||||
- Документация обновлена.
|
||||
178
docs/work-items/ORCH-066/02-trz.md
Normal file
178
docs/work-items/ORCH-066/02-trz.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# 02 — Техническое задание (ТЗ)
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Стадия:** analysis → (вход для architecture)
|
||||
**Автор:** Analyst
|
||||
|
||||
> ТЗ фиксирует ТРЕБУЕМОЕ ПОВЕДЕНИЕ и затронутые точки кода. Конкретную архитектуру
|
||||
> резолвера (точные имена ключей/функций) финализирует архитектор в ADR. Ниже —
|
||||
> опорный контракт, согласованный с бизнес-запросом Owner.
|
||||
|
||||
---
|
||||
|
||||
## 1. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль в задаче |
|
||||
|--------|---------------|
|
||||
| `src/plane_sync.py` | **Ядро изменений (слой B):** реестр логических статусов (`_DEFAULT_STATES`), `_PLANE_NAME_TO_KEY`, маппинг стадия→статус (`_STAGE_TO_STATE_KEY`, `STAGE_VISIBILITY_STATE`), хелперы `set_issue_*`. |
|
||||
| `src/webhooks/plane.py` | Маршрутизация входящего статуса (`handle_issue_updated`): `To Analyse` → `handle_status_start` (старт **или** resume). |
|
||||
| `src/stage_engine.py` | Точки ручной простановки статуса: analyst-flow (`Analysis`/`Needs Input`/`In Review`), Phase A (`Awaiting Deploy`), Phase B (`Deploying`), Phase C → `Monitoring after Deploy`, post-deploy monitor → `Done`/`Blocked`. |
|
||||
| `src/reconciler.py` | F-2 запрос статусов (`To Analyse` в список), Guard 2 skip-list (активные ожидания). |
|
||||
| `src/stages.py` | **НЕ менять** (инвариант слоя A). Используется только для чтения переходов. |
|
||||
| `src/config.py` | (При необходимости) kill-switch для новой статусной модели — на усмотрение архитектора (см. §6). |
|
||||
|
||||
---
|
||||
|
||||
## 2. Изменения статусной модели (слой B)
|
||||
|
||||
### 2.1. Реестр логических статусов (`src/plane_sync.py`)
|
||||
|
||||
Ввести новые **логические ключи** и их имена в `_PLANE_NAME_TO_KEY`:
|
||||
|
||||
| Логический ключ | Plane name | Назначение |
|
||||
|-----------------|-----------|------------|
|
||||
| `to_analyse` | `To Analyse` | Вход-триггер (старт + resume аналитика). |
|
||||
| `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` (backward-compat), `needs_input`,
|
||||
`in_review`, `blocked`, `done`, `cancelled`, `architecture`, `development`, `testing`,
|
||||
`approved`, `rejected`. `Cancelled` уже присутствует в `_PLANE_NAME_TO_KEY`.
|
||||
|
||||
### 2.2. Fail-closed резолюция (КРИТИЧНО — BR-12)
|
||||
|
||||
`get_project_states()` после резолва по API делает `setdefault(k, v)` из `_DEFAULT_STATES`.
|
||||
Чтобы отсутствие нового статуса в проекте (enduro / Plane down / частичная конфигурация)
|
||||
**не ломало** конвейер, новые логические ключи в `_DEFAULT_STATES` должны
|
||||
**алиаситься на существующие UUID** (degrade-to-current):
|
||||
|
||||
| Новый ключ | Default-алиас (UUID) | Деградированное поведение |
|
||||
|------------|----------------------|---------------------------|
|
||||
| `to_analyse` | = `in_progress` | enduro/старый проект: `In Progress` по-прежнему триггерит старт/resume. |
|
||||
| `analysis` | = `in_progress` | analysis показывается как `In Progress` (как сейчас). |
|
||||
| `code_review` | = `review` | review показывается как `Review` (как сейчас). |
|
||||
| `awaiting_deploy` | = `in_review` | Phase A показывается как `In Review` (как сейчас). |
|
||||
| `deploying` | = `in_progress` | Phase B показывается как `In Progress` (как сейчас). |
|
||||
| `monitoring` | = `done` | Phase C показывается как `Done` (как сейчас); монитор затем держит Done / флипает Blocked. |
|
||||
|
||||
> Эффект: если оператор НЕ создал новый статус — система работает строго как до ORCH-066
|
||||
> (никаких падений, никаких 404 от Plane PATCH). Если создал — резолвится по имени и
|
||||
> используется новый UUID. Это ровно паттерн ORCH-059 AC-7.
|
||||
|
||||
### 2.3. Маппинг стадия → статус
|
||||
|
||||
`src/plane_sync.py`:
|
||||
- `_STAGE_TO_STATE_KEY`: `analysis` → `analysis` (было `in_progress`); `review` → `code_review`
|
||||
(было `review`). `deploy` остаётся (управляется Phase A/B/C напрямую, не через
|
||||
`update_issue_state`). `created`/`architecture`/`development`/`testing`/`done` — без изменений.
|
||||
- `STAGE_VISIBILITY_STATE`: `review` → `code_review` (было `review`). Добавить
|
||||
`analysis` → `analysis`, если индикация analysis ставится через `set_issue_stage_state`
|
||||
(решает архитектор; альтернатива — отдельный хелпер `set_issue_analysis`).
|
||||
- Сохранить совместимость `STAGE_TO_STATE` / `PLANE_STATES` алиасов (импортируются тестами).
|
||||
|
||||
### 2.4. Точки простановки статуса
|
||||
|
||||
| Место (файл:симв.) | Сейчас | Должно стать |
|
||||
|--------------------|--------|--------------|
|
||||
| `webhooks/plane.py` `handle_issue_updated` | `new_state == in_progress` → `handle_status_start` | `new_state == to_analyse` (с fail-closed: при алиасе совпадает с `in_progress`) → `handle_status_start` |
|
||||
| `webhooks/plane.py` `start_pipeline` (старт) | статус остаётся `In Progress` | при старте/enqueue analyst орк ставит `Analysis` |
|
||||
| `webhooks/plane.py` `handle_status_start` (resume из Needs Input) | relaunch на `In Progress`-триггере | relaunch на `To Analyse`-триггере; при relaunch орк ставит `Analysis`. Fork «старт vs resume» (по `get_task_by_plane_id` + `has_active_job_for_task`) — **сохранить как есть.** |
|
||||
| `stage_engine.py` `_handle_analysis_approved_flow` (artifacts ready) | `set_issue_in_review` | оставить `In Review` (BR-9: In Review только за approve-pending конвейера) ✔ без изменений |
|
||||
| `stage_engine.py` `_handle_analysis_approved_flow` (questions) | `set_issue_needs_input` | **без изменений** (BR-10) |
|
||||
| `stage_engine.py` `_handle_self_deploy_phase_a` | `set_issue_in_review` | `Awaiting Deploy` (`set_issue_awaiting_deploy` или аналог) |
|
||||
| `stage_engine.py` `_handle_self_deploy_phase_b` | (статус не меняет) | `Deploying` |
|
||||
| `stage_engine.py` advance `deploy → done` (terminal-sync, строка ~338) | `set_issue_done` для всех | **self-hosting:** `Monitoring after Deploy` (перед/вместо арма монитора); **не-self:** `Done` как сейчас |
|
||||
| `stage_engine.py` `run_post_deploy_monitor` (HEALTHY, окно закрыто) | пишет лог + коммент, статус Plane НЕ трогает (остаётся Done) | `Done` (явно) |
|
||||
| `stage_engine.py` `run_post_deploy_monitor` (DEGRADED) | пишет лог + alert | `Blocked` |
|
||||
|
||||
> **Замечание по terminal-sync (важно для архитектора):** сейчас `advance_stage` на
|
||||
> `next_stage == "done"` вызывает `set_issue_done` безусловно (строка ~338), затем армит
|
||||
> post-deploy monitor для self-hosting (~361). Нужно развести: для репо, где
|
||||
> `post_deploy.post_deploy_applies(repo)` истинно (self-hosting) — ставить `Monitoring
|
||||
> after Deploy` вместо `Done`, и переложить простановку `Done`/`Blocked` на финал
|
||||
> монитора (`run_post_deploy_monitor`). Для прочих репо — `Done` как сейчас.
|
||||
|
||||
### 2.5. Новые хелперы `src/plane_sync.py`
|
||||
|
||||
Добавить тонкие обёртки по образцу `set_issue_in_review` (резолв per-project UUID +
|
||||
`_set_issue_state_direct`), never-raise при отсутствии issue:
|
||||
- `set_issue_analysis(work_item_id, project_id=None)`
|
||||
- `set_issue_code_review(...)` (или через `set_issue_stage_state("review")`)
|
||||
- `set_issue_awaiting_deploy(...)`
|
||||
- `set_issue_deploying(...)`
|
||||
- `set_issue_monitoring(...)`
|
||||
|
||||
(Точный набор/именование — на усмотрение архитектора; контракт: per-project резолв +
|
||||
fail-closed.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Изменения reconciler (`src/reconciler.py`)
|
||||
|
||||
- **F-2** `_reconcile_plane_project`: добавить `to_analyse` в список запрашиваемых
|
||||
статусов (`list_issues_by_state([... , to_analyse])`) и в `_reconcile_plane_issue`
|
||||
маршрутизировать `new_state == to_analyse` → `handle_status_start` (старт при `task is
|
||||
None`, resume при существующем task без active-job — логика уже в `handle_status_start`).
|
||||
Сохранить обработку `approved`/`rejected`. При fail-closed алиасе `to_analyse==in_progress`
|
||||
поведение не дублируется (один и тот же UUID).
|
||||
- **Guard 2** `_is_blocked_or_needs_input` (F-1 skip): расширить skip-множество активными
|
||||
ожиданиями — `awaiting_deploy`, `deploying`, `monitoring` — чтобы реконсилер НЕ
|
||||
«оживлял» их как зависшие (BR-13). Имя метода/семантику можно обобщить
|
||||
(«human-or-active-wait»), флаг `reconcile_skip_blocked_enabled` продолжает управлять
|
||||
этим networked-чеком.
|
||||
|
||||
> Примечание: F-1 и так не тронет Phase A (`check_deploy_status` red → silent),
|
||||
> Deploying (active finalizer job), Monitoring (стадия `done`). Guard 2 — явная
|
||||
> defense-in-depth по требованию Owner.
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения API / эндпоинтов
|
||||
|
||||
**Нет** новых HTTP-эндпоинтов. `GET /queue` / `GET /status` — без изменений контракта
|
||||
(статусы Plane там не отражаются). Изменения только во внешней индикации Plane (PATCH
|
||||
issue state — существующий механизм).
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
|
||||
**Нет.** `tasks` не хранит Plane-статус (источник истины — стадия в БД + Plane API).
|
||||
Миграции не требуются.
|
||||
|
||||
---
|
||||
|
||||
## 6. Требования к новым QG checks
|
||||
|
||||
**Нет.** `QG_CHECKS` не расширяется. Статусы — индикация, не управление (канон:
|
||||
машинные вердикты читаются из YAML-frontmatter артефактов, не из Plane-статуса).
|
||||
|
||||
Опционально (на усмотрение архитектора): единый kill-switch новой статусной модели
|
||||
(env-флаг) для безопасного раската, по образцу `staging_infra_tolerance_enabled` /
|
||||
`reconcile_skip_blocked_enabled`. Не обязателен, т.к. fail-closed алиасинг (§2.2) уже даёт
|
||||
backward-compatible деградацию.
|
||||
|
||||
---
|
||||
|
||||
## 7. Артефакты pipeline, создаваемые/обновляемые
|
||||
|
||||
- `06-adr/ADR-001-plane-status-model.md` — архитектор (решение по резолверу,
|
||||
алиасингу, разводке terminal-sync).
|
||||
- `07-infra-requirements.md` — архитектор (список Plane-статусов для ручного создания
|
||||
оператором + Plane API инструкция).
|
||||
- Документация (golden source, тот же PR): `CLAUDE.md` (секция статусной модели),
|
||||
`docs/architecture/README.md` (секция статусов рядом с ORCH-036/ORCH-021),
|
||||
`CHANGELOG.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Инварианты (проверяемые)
|
||||
|
||||
- `src/stages.py` `STAGE_TRANSITIONS` — байт-в-байт без изменений.
|
||||
- `QG_CHECKS`, `check_deploy_status`/`_parse_deploy_status`, exit-коды хука, merge-gate,
|
||||
схема БД, `Confirm Deploy`, механизм `Needs Input` — без изменений.
|
||||
- Все новые `set_issue_*` / резолв — never-raise (Plane down ⇒ degrade, не crash).
|
||||
- Поведение enduro (не-self) и его терминальный `Done` — без регресса.
|
||||
71
docs/work-items/ORCH-066/03-acceptance-criteria.md
Normal file
71
docs/work-items/ORCH-066/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 03 — Критерии приёмки (Acceptance Criteria)
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
|
||||
Каждый критерий — чёткое условие PASS/FAIL. Покрытие тестами — см. `04-test-plan.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## Группа A — Вход и стадия анализа
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-1** | `To Analyse` запускает конвейер | Перевод issue без task в `To Analyse` → `handle_status_start` → `start_pipeline` (создаётся task, ветка, enqueue analyst). | Не запускается / запускается на другом статусе. |
|
||||
| **AC-2** | `To Analyse` делает resume аналитика из Needs Input | Существующий task без active-job + перевод в `To Analyse` → relaunch агента текущей стадии (analyst читает свежие комменты). Fork «старт vs resume» определяется по `get_task_by_plane_id` + `has_active_job_for_task` (как раньше). | Создаётся второй task / двойной запуск / resume не происходит. |
|
||||
| **AC-3** | Стадия `analysis` индицируется статусом `Analysis` | При старте/relaunch аналитика орк ставит `Analysis`. | Остаётся `In Progress` (при наличии статуса `Analysis` в проекте). |
|
||||
| **AC-4** | Busy-guard сохранён | `To Analyse` при существующем active-job для task → НЕ relaunch (no double launch). | Двойной запуск агента. |
|
||||
|
||||
## Группа B — Code-Review
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-5** | Стадия `review` индицируется `Code-Review` | Вход в стадию `review` → Plane-статус `Code-Review`. | Остаётся `Review` (при наличии нового статуса). |
|
||||
|
||||
## Группа C — Self-deploy фазы
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-6** | Phase A → `Awaiting Deploy` | `_handle_self_deploy_phase_a` ставит `Awaiting Deploy` (не `In Review`). | Ставит `In Review` (при наличии нового статуса). |
|
||||
| **AC-7** | Phase B → `Deploying` | `_handle_self_deploy_phase_b` при успешном `initiate_deploy` ставит `Deploying`. | Статус не меняется / иной. |
|
||||
| **AC-8** | Phase C → `Monitoring after Deploy` (self) | Финализатор SUCCESS для self-hosting → статус `Monitoring after Deploy`, НЕ `Done` сразу. | Ставит `Done` немедленно (для self-hosting). |
|
||||
| **AC-9** | Не-self deploy → `Done` без регресса | Для не-self репо (`post_deploy_applies==False`) терминальный `deploy → done` ставит `Done` как сейчас. | Не-self репо получает `Monitoring after Deploy` / иной регресс. |
|
||||
|
||||
## Группа D — Post-deploy monitor
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-10** | Чистое окно → `Done` | `run_post_deploy_monitor` HEALTHY + окно исчерпано → статус `Done`. | Остаётся `Monitoring after Deploy` / иной. |
|
||||
| **AC-11** | Деградация → `Blocked` | `run_post_deploy_monitor` DEGRADED → статус `Blocked` (+ существующий ALERT_ONLY для self). | Остаётся в Monitoring / ставит Done. |
|
||||
| **AC-12** | Self-hosting монитор не рестартит прод | Тик НИКОГДА не рестартит/откатывает прод-контейнер (ORCH-021 BR-5 сохранён). | Тик трогает прод-контейнер. |
|
||||
|
||||
## Группа E — In Review / Needs Input / ветки
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-13** | `In Review` только за approve-pending конвейера | `In Review` ставится лишь для approve артефактов (analyst BRD/ревью), не для Phase A. | Phase A / иные стадии ставят `In Review`. |
|
||||
| **AC-14** | `Needs Input` без изменений | Поведение `set_issue_needs_input` (analyst `01-questions.md`) идентично прежнему; не расширено на других агентов. | Механизм изменён / расширен. |
|
||||
| **AC-15** | `Cancelled` — валидный выход из In Review без действий конвейера | Перевод в `Cancelled` → орк не выполняет advance/rollback (индикация, не управление). | Орк совершает действие конвейера на `Cancelled`. |
|
||||
|
||||
## Группа F — Fail-closed (КРИТИЧНО)
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-16** | Отсутствие нового статуса не ломает конвейер | Проект без новых статусов (enduro/частичный/Plane down) → `get_project_states` отдаёт default-алиасы; все `set_issue_*`/триггеры работают backward-compatible, без исключений и без 404 PATCH. | Падение / необработанное исключение / зависание задачи. |
|
||||
| **AC-17** | enduro `In Progress` по-прежнему стартует конвейер | Через `to_analyse`-алиас (= `in_progress` UUID) перевод enduro-issue в `In Progress` запускает старт/resume. | enduro-старт сломан. |
|
||||
| **AC-18** | Резолв по имени | При наличии статуса в проекте по `name` (`_PLANE_NAME_TO_KEY`) используется его UUID, а не default-алиас. | Используется неверный UUID. |
|
||||
|
||||
## Группа G — Reconciler
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-19** | F-2 реконсилирует `To Analyse` | `_reconcile_plane_project` запрашивает `to_analyse` и маршрутизирует к `handle_status_start` (старт/resume при потерянном webhook). | `To Analyse`-старты не реконсилируются. |
|
||||
| **AC-20** | Guard 2 skip активных ожиданий | Задачи в `Awaiting Deploy` / `Deploying` / `Monitoring after Deploy` НЕ «оживляются» F-1 как зависшие. | Реконсилер advance'ит активное ожидание. |
|
||||
|
||||
## Группа H — Инварианты и документация
|
||||
|
||||
| ID | Критерий | PASS | FAIL |
|
||||
|----|----------|------|------|
|
||||
| **AC-21** | `STAGE_TRANSITIONS` не изменён | `src/stages.py` `STAGE_TRANSITIONS` идентичен (diff пуст). | Любое изменение слоя A. |
|
||||
| **AC-22** | Реестры/контракты не изменены | `QG_CHECKS`, `check_deploy_status`, exit-коды хука, merge-gate, схема БД, `Confirm Deploy` — без изменений. | Любое изменение перечисленного. |
|
||||
| **AC-23** | Тесты зелёные | `pytest tests/ -q` проходит полностью; новые fail-closed тесты присутствуют и зелёные. | Любой красный тест. |
|
||||
| **AC-24** | Документация обновлена (golden source) | `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` обновлены; заведён `06-adr/ADR-001-*`. | Любой из артефактов не обновлён. |
|
||||
184
docs/work-items/ORCH-066/04-test-plan.yaml
Normal file
184
docs/work-items/ORCH-066/04-test-plan.yaml
Normal file
@@ -0,0 +1,184 @@
|
||||
work_item: ORCH-066
|
||||
description: >
|
||||
Тест-план статусной модели Plane (слой B). Покрывает осмысленные статусы этапов,
|
||||
возврат аналитика через To Analyse, фазы self-deploy, post-deploy monitor,
|
||||
fail-closed деградацию и reconciler. Слой A (STAGE_TRANSITIONS) проверяется на
|
||||
неизменность. Все тесты — pytest; Plane API мокается (httpx), как в существующих
|
||||
tests/test_plane_*.py / tests/test_orch10_states.py.
|
||||
|
||||
tests:
|
||||
# --- Группа A: вход и стадия анализа ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "To Analyse без существующего task -> handle_status_start -> start_pipeline (старт конвейера)."
|
||||
module: tests/test_status_trigger.py
|
||||
covers: [AC-1]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: integration
|
||||
description: "To Analyse при существующем task без active-job -> relaunch агента стадии (resume из Needs Input), новый task НЕ создаётся."
|
||||
module: tests/test_plane_to_analyse_resume.py
|
||||
covers: [AC-2, BR-11]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Старт/relaunch аналитика ставит Plane-статус Analysis (а не In Progress) при наличии статуса в проекте."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-3]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "To Analyse при существующем task с active-job -> НЕ relaunch (busy-guard)."
|
||||
module: tests/test_plane_to_analyse_resume.py
|
||||
covers: [AC-4]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа B: Code-Review ---
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "Вход в стадию review -> Plane-статус Code-Review (маппинг _STAGE_TO_STATE_KEY / STAGE_VISIBILITY_STATE)."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-5]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа C: self-deploy фазы ---
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "_handle_self_deploy_phase_a ставит Awaiting Deploy (не In Review)."
|
||||
module: tests/test_deploy_approve.py
|
||||
covers: [AC-6, AC-13]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "_handle_self_deploy_phase_b при успешном initiate_deploy ставит Deploying."
|
||||
module: tests/test_deploy_approve.py
|
||||
covers: [AC-7]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: integration
|
||||
description: "Phase C (finalizer SUCCESS) для self-hosting ставит Monitoring after Deploy, НЕ Done; армит post-deploy monitor."
|
||||
module: tests/test_deploy_terminal_sync.py
|
||||
covers: [AC-8]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: "Не-self репо: deploy->done ставит Done (без регресса, Monitoring не применяется)."
|
||||
module: tests/test_deploy_terminal_sync.py
|
||||
covers: [AC-9]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа D: post-deploy monitor ---
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "run_post_deploy_monitor HEALTHY + окно исчерпано -> Plane-статус Done."
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-10]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "run_post_deploy_monitor DEGRADED -> Plane-статус Blocked (+ ALERT_ONLY для self)."
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-11]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Self-hosting тик НЕ рестартит/не откатывает прод-контейнер (ORCH-021 BR-5 сохранён)."
|
||||
module: tests/test_post_deploy.py
|
||||
covers: [AC-12]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа E: In Review / Needs Input / Cancelled ---
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "In Review ставится только за approve-pending конвейера (analyst BRD ready), не Phase A."
|
||||
module: tests/test_analyst_status_only_regression.py
|
||||
covers: [AC-13]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "set_issue_needs_input (analyst 01-questions.md) поведение идентично прежнему; не расширено на других агентов."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-14, BR-10]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "Перевод в Cancelled -> handle_issue_updated не выполняет advance/rollback (индикация, не управление)."
|
||||
module: tests/test_plane_webhook.py
|
||||
covers: [AC-15]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа F: fail-closed (критично) ---
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: "Проект без новых статусов: get_project_states отдаёт default-алиасы (to_analyse=in_progress, code_review=review, awaiting_deploy=in_review, monitoring=done); исключений нет."
|
||||
module: tests/test_plane_status_failclosed.py
|
||||
covers: [AC-16, BR-12]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: unit
|
||||
description: "Plane API down -> get_project_states fallback на _DEFAULT_STATES; set_issue_* never-raise."
|
||||
module: tests/test_plane_status_failclosed.py
|
||||
covers: [AC-16]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "enduro In Progress по-прежнему стартует конвейер через to_analyse-алиас."
|
||||
module: tests/test_plane_status_failclosed.py
|
||||
covers: [AC-17]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: unit
|
||||
description: "Резолв по имени: при наличии статуса в проекте используется его UUID, а не default-алиас."
|
||||
module: tests/test_orch10_states.py
|
||||
covers: [AC-18]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа G: reconciler ---
|
||||
- id: TC-20
|
||||
type: integration
|
||||
description: "F-2 _reconcile_plane_project запрашивает to_analyse и маршрутизирует к handle_status_start (потерянный webhook старта/resume)."
|
||||
module: tests/test_reconciler_plane.py
|
||||
covers: [AC-19]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: "Guard 2: задачи в Awaiting Deploy / Deploying / Monitoring after Deploy НЕ оживляются F-1 как зависшие."
|
||||
module: tests/test_reconciler.py
|
||||
covers: [AC-20, BR-13]
|
||||
expected: PASS
|
||||
|
||||
# --- Группа H: инварианты ---
|
||||
- id: TC-22
|
||||
type: unit
|
||||
description: "STAGE_TRANSITIONS не изменён (явная проверка ключей/значений слоя A)."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-21]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-23
|
||||
type: unit
|
||||
description: "QG_CHECKS реестр и check_deploy_status контракты не изменены."
|
||||
module: tests/test_plane_status_model.py
|
||||
covers: [AC-22]
|
||||
expected: PASS
|
||||
|
||||
- id: TC-24
|
||||
type: integration
|
||||
description: "Полный прогон pytest tests/ -q зелёный (регрессия)."
|
||||
module: tests/
|
||||
covers: [AC-23]
|
||||
expected: PASS
|
||||
287
docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md
Normal file
287
docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# 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 его
|
||||
**базового ключа из этого же проекта**:
|
||||
|
||||
```python
|
||||
_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_state`→`stage_to_state`):
|
||||
`analysis` → `analysis` (было `in_progress`); `review` → `code_review` (было `review`).
|
||||
`deploy` остаётся `in_progress` (управляется Phase A/B/C напрямую). Остальные — без
|
||||
изменений.
|
||||
- `STAGE_VISIBILITY_STATE`: `review` → `code_review`; добавить `analysis` → `analysis`
|
||||
(для консистентности; `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_progress` → `handle_status_start` | `new_state == to_analyse` → `handle_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)`:
|
||||
|
||||
```python
|
||||
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_progress` → `to_analyse` в
|
||||
списке запрашиваемых статусов (`list_issues_by_state([to_analyse, approved, rejected])`)
|
||||
и в `_reconcile_plane_issue` маршрутизировать `new_state == to_analyse` →
|
||||
`handle_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 **только когда
|
||||
они РАЗЛИЧНЫ от базовых рабочих статусов проекта** (т.е. реально созданы):
|
||||
|
||||
```python
|
||||
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_progress`→`to_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_progress`→`set_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);
|
||||
статусы — индикация, отделены от машины стадий.
|
||||
96
docs/work-items/ORCH-066/07-infra-requirements.md
Normal file
96
docs/work-items/ORCH-066/07-infra-requirements.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# 07 — Требования к инфраструктуре
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Автор:** Architect
|
||||
**Дата:** 2026-06-07
|
||||
|
||||
> ORCH-066 не меняет топологию (контейнеры/порты/сеть — без изменений, см.
|
||||
> `docs/operations/INFRA.md`). Единственное инфра-действие — создание новых
|
||||
> Plane-статусов в проекте **ORCH** руками оператора через Plane API. Это
|
||||
> **предусловие эксплуатации**, не часть кодового PR.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что нужно сделать оператору (ДО эксплуатации новой модели)
|
||||
|
||||
Создать в Plane-проекте **ORCH** следующие статусы (states) с точными именами —
|
||||
резолвер сопоставляет их по `name` (`_PLANE_NAME_TO_KEY`):
|
||||
|
||||
| Plane name (точно) | Логический ключ | Группа Plane (рекомендуемая) | Назначение |
|
||||
|--------------------|-----------------|------------------------------|------------|
|
||||
| `To Analyse` | `to_analyse` | unstarted / started | Человеческий вход: старт конвейера + resume аналитика из Needs Input. |
|
||||
| `Analysis` | `analysis` | started | Индикация стадии анализа. |
|
||||
| `Code-Review` | `code_review` | started | Индикация стадии review. |
|
||||
| `Awaiting Deploy` | `awaiting_deploy` | started | Phase A: ожидание ручного approve на прод-деплой. |
|
||||
| `Deploying` | `deploying` | started | Phase B: идёт прод-деплой. |
|
||||
| `Monitoring after Deploy` | `monitoring` | started | Phase C / окно пост-деплой наблюдения. |
|
||||
|
||||
`Confirm Deploy` (ORCH-059) и базовые статусы (`Backlog`, `Todo`, `In Progress`,
|
||||
`Architecture`, `Development`, `Review`, `Testing`, `Approved`, `Rejected`, `Done`,
|
||||
`Cancelled`, `Needs Input`, `In Review`, `Blocked`) уже существуют — **не трогать**.
|
||||
|
||||
> ⚠️ **Точность имён критична.** Резолв идёт по строковому `name`. Опечатка/иной регистр
|
||||
> → статус не сопоставится → ключ деградирует на собственный базовый UUID проекта
|
||||
> (alias-fallback, ADR §2.2): индикация откатится к старому статусу, но конвейер
|
||||
> продолжит работать. Дефис в `Code-Review` — обязателен.
|
||||
|
||||
---
|
||||
|
||||
## 2. Plane API — как создать статус
|
||||
|
||||
Эндпоинт (как в `src/plane_sync.py`, `PLANE_BASE = {plane_api_url}/api/v1`):
|
||||
|
||||
```
|
||||
POST {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/states/
|
||||
Headers: X-API-Key: <PLANE_API_TOKEN> (или соответствующий бот-токен с правами)
|
||||
Body (JSON):
|
||||
{ "name": "To Analyse", "group": "started", "color": "#3f76ff" }
|
||||
```
|
||||
|
||||
Повторить для каждого имени из таблицы §1. `group` влияет только на колонку доски;
|
||||
оркестратор `group` не читает (резолв строго по `name`). `color` — на вкус оператора.
|
||||
|
||||
Проверка после создания:
|
||||
|
||||
```
|
||||
GET {PLANE_BASE}/workspaces/{WORKSPACE}/projects/{ORCH_PROJECT_ID}/states/
|
||||
```
|
||||
В ответе должны присутствовать все 6 имён.
|
||||
|
||||
---
|
||||
|
||||
## 3. Сброс кэша статусов (важно)
|
||||
|
||||
`get_project_states` кэширует резолв per-process (`_STATES_CACHE`). После создания
|
||||
статусов оркестратор подхватит их **только** после сброса кэша:
|
||||
|
||||
- штатно — `plane_sync.reload_project_states(project_id)` (или рестарт процесса);
|
||||
- на **staging** (8501) — безопасный рестарт песочницы;
|
||||
- на **прод** (8500) — **НЕ рестартить контейнер ради этого** в рамках задачи
|
||||
(self-hosting: общий контейнер всех проектов). Кэш заполняется при первом обращении к
|
||||
проекту; если статусы созданы ДО первого PATCH в цикле новой версии — отдельный сброс не
|
||||
нужен. Если созданы позже — дождаться штатного цикла обновления/деплоя орка.
|
||||
|
||||
---
|
||||
|
||||
## 4. Порядок раската (рекомендация)
|
||||
|
||||
1. Слить кодовый PR ORCH-066 через `deploy-staging` (8501).
|
||||
2. Создать 6 статусов в проекте ORCH (§1–§2).
|
||||
3. Сбросить кэш / поднять staging, прогнать sandbox-задачу — убедиться, что доска
|
||||
показывает `Analysis` / `Code-Review` / `Awaiting Deploy` / `Deploying` /
|
||||
`Monitoring after Deploy` / `Done` на соответствующих этапах.
|
||||
4. Прод-деплой орка штатным self-deploy (Phase A → approve → Phase B/C).
|
||||
|
||||
**До шага 2** система работает строго как до ORCH-066 (alias-fallback) — раскат
|
||||
безопасно обратим: не создавать/удалить статусы = откат индикации к старой модели,
|
||||
без изменения кода.
|
||||
|
||||
---
|
||||
|
||||
## 5. Что НЕ требуется
|
||||
|
||||
- Никаких изменений docker-compose, портов, сети, томов, `.env`/`.env.staging`.
|
||||
- Никаких миграций БД (`tasks` не хранит Plane-статус).
|
||||
- Никаких изменений в проекте **enduro-trails** — там новые статусы не создаются;
|
||||
alias-fallback держит прежнюю индикацию (`In Progress`/`Review`/`Done`).
|
||||
31
docs/work-items/ORCH-066/10-tech-risks.md
Normal file
31
docs/work-items/ORCH-066/10-tech-risks.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 10 — Технические риски
|
||||
|
||||
**Work Item:** ORCH-066
|
||||
**Автор:** Architect
|
||||
**Дата:** 2026-06-07
|
||||
|
||||
Риски слоя B (Plane-индикация). Слой A (`STAGE_TRANSITIONS`/гейты) не затрагивается, поэтому
|
||||
класс «сломали конвейер» структурно исключён — худший исход любого риска ниже = неверная
|
||||
**индикация**, не остановка конвейера.
|
||||
|
||||
| ID | Риск | Вероятность | Влияние | Митигация |
|
||||
|----|------|-------------|---------|-----------|
|
||||
| **R1** | Частичная конфигурация: оператор создал не все 6 статусов в ORCH → отсутствующий ключ деградирует. Наивный статический enduro-UUID дал бы невалидный `state` (PATCH 422) на orchestrator-issue. | Средняя | Средн. | **Project-relative alias-fallback** (ADR §2.2): отсутствующий ключ → собственный базовый UUID проекта → PATCH валиден, индикация откатывается к текущему статусу. Покрыть тестом partial-config. |
|
||||
| **R2** | Enduro-регресс через Guard 2: новые ключи алиасятся на `in_progress`/`in_review`/`done`; наивное добавление в skip-set заставит F-1 пропускать enduro `In Progress`/`Done` → сломанная реконсиляция (ORCH-053/060). | Средняя | Высок. | **Subtraction базовых рабочих статусов** (ADR §2.6): `extra_waits -= base_working`. На enduro (алиасы схлопнуты) `extra_waits == {}` → нулевой регресс. Тест: enduro-алиас не добавляет skip, orchestrator-distinct добавляет. |
|
||||
| **R3** | Двойной триггер старта: F-2 reconciler и webhook оба маршрутизируют `to_analyse`; при алиасе `to_analyse == in_progress` возможен повтор. | Низкая | Низк. | `list_issues_by_state` дедуплицирует UUID через `set`; active-job guard + atomic create-claim в `handle_status_start` (`get_task_by_plane_id` + `has_active_job_for_task`) — без двойного старта (AC-4). Сохранить fork как есть. |
|
||||
| **R4** | Кэш статусов: после создания статусов `_STATES_CACHE` отдаёт старый резолв до сброса → доска не обновляется. | Средняя | Низк. | `reload_project_states()` / рестарт **staging**. Документировано в `07-infra-requirements.md §3`. Прод-рестарт ради кэша — запрещён (self-hosting). |
|
||||
| **R5** | Опечатка в имени статуса оператором (`Code Review` без дефиса и т.п.) → ключ не резолвится. | Средняя | Низк. | Резолв по точному `name`; при промахе — alias-fallback (деградация, не падение). Точные имена и проверка в `07-infra-requirements.md §1–2`. |
|
||||
| **R6** | Terminal-sync split: ошибка ветвления `post_deploy_applies` → enduro получает `Monitoring after Deploy` вместо `Done` (регресс AC-9) или self уходит в `Done` минуя окно (AC-8). | Низкая | Средн. | Единый источник условности — `post_deploy.post_deploy_applies(repo)` (та же функция, что армит монитор). Тесты AC-8 (self→monitoring) и AC-9 (не-self→done). |
|
||||
| **R7** | Phase B: `set_issue_deploying` поставлен до фактического старта детача → ложная индикация при провале `initiate_deploy`. | Низкая | Низк. | Ставить `set_issue_deploying` **после** успешного `initiate_deploy` и записи `INITIATED` marker (ADR §2.5); провал `initiate_deploy` оставляет `Awaiting Deploy` + просьбу повторить approve. |
|
||||
| **R8** | Post-deploy DEGRADED → `set_issue_blocked` ошибочно трактуется как «действие над продом». | Низкая | Высок.(если) | `set_issue_blocked` — только Plane-PATCH. Тик остаётся `ALERT_ONLY`, НИКОГДА не рестартит/откатывает прод-контейнер (AC-12, ORCH-021 BR-5). Явный тест: self DEGRADED не трогает контейнер. |
|
||||
| **R9** | Plane API недоступен в момент простановки статуса → PATCH падает. | Низкая | Низк. | Все `set_issue_*`/`_set_issue_state_direct` — never-raise (логируют, не пробрасывают). Индикация пропускается, слой A не затронут. |
|
||||
| **R10** | Регресс на тестах, читающих `STAGE_TO_STATE`/`PLANE_STATES` конкретные UUID. | Низкая | Низк. | Новые ключи в `_DEFAULT_STATES` = алиасы на те же in_progress/review/done UUID → значения байт-в-байт; `STAGE_TO_STATE` analysis/review остаются прежними UUID (ADR §2.3). |
|
||||
| **R11** | Self-hosting: выкладка орка минуя staging. | Низкая | Высок. | Обязательный `deploy-staging` гейт (8501); прод не рестартить вне штатного self-deploy. Раскат обратим (не создавать статусы = старое поведение). |
|
||||
|
||||
## Сводный вывод
|
||||
|
||||
Все риски снижаемы в рамках принятой архитектуры; ни один не способен остановить конвейер
|
||||
(слой A инвариантен). Два ключевых требуют аккуратной реализации и обязательных тестов:
|
||||
**R1** (project-relative alias-fallback) и **R2** (Guard-2 anti-regress subtraction) —
|
||||
оба зафиксированы в ADR §2.2 и §2.6 как явные контракты. Эскалации `arch:major-change` не
|
||||
требуется: изменение локализовано в слое B, без новых компонентов/стадий/QG/миграций.
|
||||
89
docs/work-items/ORCH-066/12-review.md
Normal file
89
docs/work-items/ORCH-066/12-review.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-066
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-066
|
||||
|
||||
## Summary
|
||||
Осмысленная статусная модель Plane (слой B — индикация). Реализация затрагивает
|
||||
строго слой B (`src/plane_sync.py`, точки простановки в `src/stage_engine.py` /
|
||||
`src/webhooks/plane.py` / `src/reconciler.py`) и **не трогает слой A**
|
||||
(`src/stages.py::STAGE_TRANSITIONS` — diff пуст). Все 4 оси проверки (ТЗ, ADR,
|
||||
качество кода, тесты) и проверка документации — пройдены. `pytest tests/ -q`:
|
||||
**774 passed**. Вердикт — **APPROVED**.
|
||||
|
||||
## Соответствие ТЗ (02-trz.md)
|
||||
- §2.1 — 6 новых логических ключей в `_PLANE_NAME_TO_KEY` + `_DEFAULT_STATES`. ✔
|
||||
- §2.2 — fail-closed резолюция (BR-12). ✔ (реализована усиленная project-relative
|
||||
версия — см. ADR ниже).
|
||||
- §2.3 — `_STAGE_TO_STATE_KEY` (analysis→analysis, review→code_review),
|
||||
`STAGE_VISIBILITY_STATE`, legacy `STAGE_TO_STATE` (UUID байт-в-байт прежние). ✔
|
||||
- §2.4 — точки простановки разведены (handle_issue_updated триггер `to_analyse`,
|
||||
start_pipeline/resume → Analysis, Phase A → Awaiting Deploy, Phase B → Deploying,
|
||||
terminal-sync split, post-deploy HEALTHY→Done / DEGRADED→Blocked,
|
||||
rollback@analysis → Analysis). ✔
|
||||
- §2.5 — 5 новых never-raise хелперов `set_issue_*`. ✔
|
||||
- §3 — reconciler F-2 триггер `to_analyse` (+ resume-ветка), Guard 2 skip-set с
|
||||
вычитанием base_working. ✔
|
||||
- §4/§5/§6 — нет новых эндпоинтов, нет миграций БД, `QG_CHECKS` не расширен. ✔
|
||||
|
||||
## Соответствие ADR (06-adr/ADR-001)
|
||||
- §2.2 project-relative alias-fallback (`_STATE_ALIAS_FALLBACK`, применён ДО
|
||||
`_DEFAULT_STATES.setdefault`) — реализован точно по контракту, деградация на
|
||||
собственный базовый UUID проекта, PATCH остаётся валидным на частичной
|
||||
конфигурации. ✔
|
||||
- §2.5 terminal-sync split по `post_deploy.post_deploy_applies(repo)` — реализован
|
||||
как в ADR (self → Monitoring, не-self → Done). ✔
|
||||
- §2.6 Guard 2 анти-регресс (extra_waits − base_working − {None}) — реализован
|
||||
дословно, enduro-алиасы схлопываются → нулевой регресс. ✔
|
||||
- §2.7 без kill-switch — config.py не изменён (diff пуст). ✔
|
||||
|
||||
## Качество кода
|
||||
- Все новые `set_issue_*` следуют образцу `set_issue_in_review` (per-project резолв
|
||||
+ `_set_issue_state_direct`), контракт never-raise сохранён, есть docstrings. ✔
|
||||
- Post-deploy/terminal-sync простановки обёрнуты в try/except с warning-логом
|
||||
(never break the tick). ✔
|
||||
- Переменные в scope корректны (`work_item_id` определён до всех новых вызовов в
|
||||
`start_pipeline`/`handle_status_start`/stage_engine). ✔
|
||||
- AC-12 соблюдён: `set_issue_blocked` в DEGRADED-ветке — только индикация, тик
|
||||
прод-контейнер не трогает. ✔
|
||||
|
||||
## Качество тестов
|
||||
- Содержательные, не тривиальные: `test_plane_status_failclosed.py`
|
||||
(TC-16/17/18 — partial project, API down, never-raise сеттеров, enduro alias
|
||||
старт), `test_plane_to_analyse_resume.py`, `test_plane_status_model.py`,
|
||||
`test_deploy_terminal_sync.py` (self/не-self split), `test_post_deploy_integration.py`,
|
||||
`test_reconciler*.py` (F-2 to_analyse + Guard 2). ✔
|
||||
|
||||
## Инварианты (AC-21/AC-22)
|
||||
- `src/stages.py` — diff 0 строк (STAGE_TRANSITIONS байт-в-байт). ✔
|
||||
- `src/qg/checks.py` — diff 0 строк (QG_CHECKS, check_deploy_status). ✔
|
||||
- `src/config.py` — diff 0 строк. ✔
|
||||
- Схема БД — без миграций. ✔
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
## Документация
|
||||
Обновлена в том же PR (golden source соблюдён):
|
||||
- `CLAUDE.md` — добавлена секция «Статусная модель Plane (ORCH-066)». ✔
|
||||
- `docs/architecture/README.md` — секция «Осмысленная статусная модель Plane
|
||||
(ORCH-066)» + обновлён статусный footer. ✔
|
||||
- `CHANGELOG.md` — подробная запись в [Unreleased]/Added. ✔
|
||||
- `06-adr/ADR-001-plane-status-model.md` — заведён. ✔
|
||||
- `07-infra-requirements.md` — присутствует (инфра-предусловие: 6 Plane-статусов
|
||||
создаёт оператор). ✔
|
||||
|
||||
Изменения `src/` полностью отражены в документации → требование
|
||||
«документация обновлена при изменении src/» выполнено.
|
||||
77
docs/work-items/ORCH-066/13-test-report.md
Normal file
77
docs/work-items/ORCH-066/13-test-report.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-066
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-066
|
||||
|
||||
Осмысленная статусная модель Plane (слой B — индикация). Прогон полного регресса +
|
||||
покрытие тест-плана `04-test-plan.yaml` + проверка инвариантов слоя A.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Ветка: feature/ORCH-066-plane
|
||||
- Дата: 2026-06-07
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Покрывает | Описание | Модуль | Результат |
|
||||
|-------|-----------|----------|--------|-----------|
|
||||
| TC-01 | AC-1 | To Analyse без task → start_pipeline | test_status_trigger.py | PASS |
|
||||
| TC-02 | AC-2,BR-11 | To Analyse resume аналитика, без двойного task | test_plane_to_analyse_resume.py | PASS |
|
||||
| TC-03 | AC-3 | Старт/relaunch → статус Analysis | test_plane_status_model.py | PASS |
|
||||
| TC-04 | AC-4 | Busy-guard: active-job → не relaunch | test_plane_to_analyse_resume.py | PASS |
|
||||
| TC-05 | AC-5 | review → статус Code-Review | test_plane_status_model.py | PASS |
|
||||
| TC-06 | AC-6,AC-13 | Phase A → Awaiting Deploy (не In Review) | test_deploy_approve.py | PASS |
|
||||
| TC-07 | AC-7 | Phase B → Deploying | test_deploy_approve.py | PASS |
|
||||
| TC-08 | AC-8 | Phase C self → Monitoring after Deploy | test_deploy_terminal_sync.py | PASS |
|
||||
| TC-09 | AC-9 | Не-self deploy→done → Done (без регресса) | test_deploy_terminal_sync.py | PASS |
|
||||
| TC-10 | AC-10 | Post-deploy HEALTHY → Done | test_post_deploy.py | PASS |
|
||||
| TC-11 | AC-11 | Post-deploy DEGRADED → Blocked | test_post_deploy.py | PASS |
|
||||
| TC-12 | AC-12 | Self-тик не рестартит прод | test_post_deploy.py | PASS |
|
||||
| TC-13 | AC-13 | In Review только за approve-pending | test_analyst_status_only_regression.py | PASS |
|
||||
| TC-14 | AC-14,BR-10 | Needs Input без изменений | test_plane_status_model.py | PASS |
|
||||
| TC-15 | AC-15 | Cancelled → нет действий конвейера | test_plane_webhook.py | PASS |
|
||||
| TC-16 | AC-16,BR-12 | Fail-closed default-алиасы, нет исключений | test_plane_status_failclosed.py | PASS |
|
||||
| TC-17 | AC-16 | Plane API down → fallback, never-raise | test_plane_status_failclosed.py | PASS |
|
||||
| TC-18 | AC-17 | enduro In Progress стартует через алиас | test_plane_status_failclosed.py | PASS |
|
||||
| TC-19 | AC-18 | Резолв по имени → корректный UUID | test_orch10_states.py | PASS |
|
||||
| TC-20 | AC-19 | F-2 реконсилирует To Analyse | test_reconciler_plane.py | PASS |
|
||||
| TC-21 | AC-20,BR-13 | Guard 2 skip активных ожиданий | test_reconciler.py | PASS |
|
||||
| TC-22 | AC-21 | STAGE_TRANSITIONS не изменён | test_plane_status_model.py | PASS |
|
||||
| TC-23 | AC-22 | QG_CHECKS/check_deploy_status не изменены | test_plane_status_model.py | PASS |
|
||||
| TC-24 | AC-23 | Полный регресс pytest зелёный | tests/ | PASS |
|
||||
|
||||
Все 24 тест-кейса — PASS.
|
||||
|
||||
## Инварианты слоя A (AC-21 / AC-22)
|
||||
Diff против `origin/main` (merge-base `4815e378`):
|
||||
- `src/stages.py` (STAGE_TRANSITIONS) — diff пуст ✔
|
||||
- `src/qg/checks.py` (QG_CHECKS, check_deploy_status) — diff пуст ✔
|
||||
- `src/config.py` (без kill-switch) — diff пуст ✔
|
||||
|
||||
## Smoke test API (TestClient — прод-контейнер 8500 не трогался)
|
||||
> `curl` в окружении недоступен; smoke прогнан через FastAPI TestClient (lifespan),
|
||||
> без рестарта/обращения к прод-контейнеру (self-hosting safety).
|
||||
|
||||
| Endpoint | Статус | Тело (фрагмент) |
|
||||
|----------|--------|-----------------|
|
||||
| GET /health | 200 | `{"status":"ok","service":"orchestrator"}` |
|
||||
| GET /status | 200 | `{"active_tasks":[...]}` |
|
||||
| GET /queue | 200 | `{"counts":{...},"max_concurrency":1,...}` |
|
||||
|
||||
## Вывод pytest
|
||||
```
|
||||
======================= 774 passed, 1 warning in 17.68s ========================
|
||||
```
|
||||
(единственный warning — PydanticDeprecatedSince20 в src/config.py, предсуществующий,
|
||||
не связан с ORCH-066)
|
||||
|
||||
Прогон по модулям тест-плана: `117 passed` (ORCH-066-специфичные файлы).
|
||||
|
||||
## Итог
|
||||
PASS — все тесты зелёные (774 passed), все 24 TC покрыты, инварианты слоя A
|
||||
сохранены (diff пуст), smoke-эндпоинты отвечают 200. Review-вердикт APPROVED.
|
||||
Задача готова к переходу на стадию deploy-staging.
|
||||
12
docs/work-items/ORCH-066/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-066/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-066
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
39
docs/work-items/ORCH-066/15-staging-log.md
Normal file
39
docs/work-items/ORCH-066/15-staging-log.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-07T22:01:57Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` instance (port 8501),
|
||||
run canonically via `docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`.
|
||||
|
||||
**Result: 8/10 checks PASS — exit code 0 (advance).**
|
||||
|
||||
All REAL (pipeline) checks are green. The two failing checks are the known
|
||||
SANDBOX_INFRA-only checks C9a/C9b (sandbox branch / analyst-job — depend on
|
||||
SANDBOX bot accounts being project members, not on the pipeline), which are
|
||||
waived under ORCH-061 since every REAL check passed.
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
## Check breakdown
|
||||
|
||||
| Block | Check | Result |
|
||||
|-------|-------|--------|
|
||||
| A SMOKE | A1 GET /health → 200 status=ok | PASS |
|
||||
| A SMOKE | A2 GET /queue → 200 with counts/max_concurrency/resilience | PASS |
|
||||
| A SMOKE | A3 ORCH_STAGING=true (not prod) | PASS |
|
||||
| B ACCESS | B4 Plane: sandbox project accessible | PASS |
|
||||
| B ACCESS | B5 Gitea: orchestrator-sandbox accessible, push=true | PASS |
|
||||
| B ACCESS | B6 Registry: sandbox present, prod ET/ORCH absent | PASS |
|
||||
| C E2E | C7 Create issue in Plane SANDBOX | PASS |
|
||||
| C E2E | C8 Trigger pipeline via /webhook/plane | PASS |
|
||||
| C E2E | C9a Branch appears in orchestrator-sandbox | FAIL (waived — sandbox-infra) |
|
||||
| C E2E | C9b Analyst job enqueued in staging queue | FAIL (waived — sandbox-infra) |
|
||||
|
||||
CLEANUP completed: test Plane issue deleted (HTTP 204); no branch to delete.
|
||||
14
docs/work-items/ORCH-066/16-post-deploy-log.md
Normal file
14
docs/work-items/ORCH-066/16-post-deploy-log.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY
|
||||
action_taken: NONE
|
||||
work_item: ORCH-066
|
||||
window_s: 900
|
||||
checks_total: 30
|
||||
checks_failed: 0
|
||||
---
|
||||
|
||||
# Post-deploy log — ORCH-021 post-deploy monitor
|
||||
|
||||
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
|
||||
|
||||
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.
|
||||
7
docs/work-items/ORCH-067/00-business-request.md
Normal file
7
docs/work-items/ORCH-067/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: [высокий] Telegram tracker: bump + статусы Plane + кликабельный номер задачи
|
||||
|
||||
Work Item ID: ORCH-067
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
158
docs/work-items/ORCH-067/01-brd.md
Normal file
158
docs/work-items/ORCH-067/01-brd.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# BRD — ORCH-067: Telegram tracker (bump + статусы Plane + кликабельный номер задачи)
|
||||
|
||||
Work Item: **ORCH-067**
|
||||
Тип: **Багфикс + enhancement**
|
||||
Приоритет: высокий
|
||||
Компонент: Telegram live-tracker и уведомления оркестратора (`src/notifications.py`)
|
||||
Расширяет: открытый баг seq=55 («bump не сработал, регресс ORCH-042»)
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
Оркестратор ведёт по одной «живой карточке» (live-tracker) на каждую задачу в Telegram
|
||||
(`src/notifications.py`). Карточка тихо обновляется на каждом переходе стадии, а отдельными
|
||||
пингами шлются только события, требующие внимания владельца (approve-gate, деплой-фейл,
|
||||
падение агента, ошибка задачи).
|
||||
|
||||
Сейчас есть четыре боли:
|
||||
|
||||
1. **bump не работает в проде.** Диагностика оператора: код режима `bump` в
|
||||
`update_task_tracker` корректен (delete старого → sendMessage вниз → repoint
|
||||
`tracker_message_id`), НО в проде `tracker_mode="edit"` (дефолт `src/config.py:408`),
|
||||
а `ORCH_TRACKER_MODE=bump` не выставлен. Карточка обновляется edit-in-place и остаётся
|
||||
«вверху» ленты, тонет под новыми сообщениями — наблюдатель не видит актуального
|
||||
состояния без скролла.
|
||||
|
||||
2. **Карточка показывает внутренние названия стадий, а не Plane-статусы.** После ввода
|
||||
осмысленной статусной модели Plane (ORCH-066) карточка по-прежнему рендерит внутренние
|
||||
ярлыки стадий (Анализ/Архитектура/…), а текущий статус задачи в терминах, понятных
|
||||
наблюдателю в Plane (To Analyse → Analysis → In Review → … → Done), в шапке карточки
|
||||
не отражён. Особенно теряется состояние **ожидания согласования BRD** = Plane-статус
|
||||
`In Review`: сейчас это лишь строка «✅/⏸️ Подтверждение BRD … ⏳», не выраженная как
|
||||
полноценный статус.
|
||||
|
||||
3. **Номер задачи в карточке некликабелен.** `ORCH-066` в карточке — обычный текст;
|
||||
чтобы открыть задачу в Plane, наблюдателю приходится искать её вручную.
|
||||
|
||||
4. **Номер задачи некликабелен и во всех остальных уведомлениях орка** (approve-requested,
|
||||
QG-fail, deploy SUCCESS/FAIL, Needs Input, прод-деплой и т. п.) — везде, где упоминается
|
||||
`work_item_id`, это просто текст.
|
||||
|
||||
## 2. Цель
|
||||
|
||||
Сделать live-tracker и уведомления орка наблюдаемыми «из коробки»:
|
||||
- bump работает по умолчанию (карточка падает вниз свежим сообщением при каждом обновлении,
|
||||
ровно одна карточка на задачу, без спама и дублей);
|
||||
- карточка явно показывает текущий Plane-статус по модели ORCH-066, включая человеческие
|
||||
гейты (`⏸️ In Review` — согласование BRD, `⏸️ Awaiting Deploy` — ожидание Confirm Deploy,
|
||||
`❓ Needs Input` — нужны уточнения);
|
||||
- номер задачи кликабелен в карточке и во всех Telegram-уведомлениях орка и ведёт на
|
||||
страницу задачи в Plane.
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
|
||||
- **Owner (Слава)** — основной потребитель карточки и уведомлений; источник 4 требований.
|
||||
- **Агенты конвейера** — косвенно (карточка отражает их прогресс; поведение агентов не меняется).
|
||||
- **Другие проекты (enduro-trails)** — общий инстанс/БД; изменения не должны вызывать регресс.
|
||||
|
||||
## 4. Объём работ (scope)
|
||||
|
||||
### 4.1. Требование 1 — bump по умолчанию
|
||||
- Режим `bump` должен быть поведением по умолчанию: при каждом обновлении карточка
|
||||
удаляется и пересоздаётся внизу ленты, одна карточка на задачу, тихо
|
||||
(`disable_notification`), без дублей.
|
||||
- Инвариант «одна карточка на задачу» сохраняется в обоих режимах (`edit` остаётся как
|
||||
опция через env).
|
||||
- Транзиентный фейл `send` не должен обнулять `tracker_message_id` и плодить дубли
|
||||
(инвариант уже заложен в коде — сохранить).
|
||||
|
||||
### 4.2. Требование 2 — статусы карточки как в Plane (модель ORCH-066)
|
||||
- В шапке/верхней части карточки явно отображается **текущий Plane-статус** задачи по
|
||||
модели ORCH-066.
|
||||
- Полный маппинг состояний (имена — финальные из модели ORCH-066):
|
||||
```
|
||||
To Analyse → Analysis → In Review (⏸️ ожидание согласования BRD) → Architecture →
|
||||
Development → Code-Review → Testing → Awaiting Deploy (⏸️ ожидание Confirm Deploy) →
|
||||
Deploying → Monitoring after Deploy → Done
|
||||
```
|
||||
Ветки: `Needs Input` (аналитик задал вопросы), `Blocked`, `Rejected`, `Cancelled`.
|
||||
- Человеческие гейты отражаются как ПОЛНОЦЕННЫЕ статусы с паузой:
|
||||
- согласование BRD → «⏸️ In Review — ожидание согласования BRD»;
|
||||
- ожидание прод-деплоя → «⏸️ Awaiting Deploy — ожидание Confirm Deploy»;
|
||||
- вопросы аналитика → «❓ Needs Input — нужны уточнения».
|
||||
- Существующая семантика строки «Подтверждение BRD» сохраняется (время ожидания/«твоё
|
||||
время»), но статус карточки при этом явно показывает In Review (approve-pending).
|
||||
|
||||
### 4.3. Требование 3 — кликабельный номер задачи в карточке
|
||||
- `work_item_id` (напр. `ORCH-066`) в карточке — гиперссылка на страницу задачи Plane:
|
||||
`https://<PLANE_WEB_BASE>/<workspace_slug>/projects/<project_id>/issues/<issue_id>/`.
|
||||
- Источники частей URL:
|
||||
- `PLANE_WEB_BASE` — из конфигурации (env, поле `plane_web_url` / `ORCH_PLANE_WEB_URL`;
|
||||
значение прод — `plane.mva154.duckdns.org`); fail-safe: не задан → номер без ссылки;
|
||||
- `workspace_slug` — `plane_workspace_slug` (уже есть в settings, прод — `ag_proj`);
|
||||
- `project_id` — резолвится per-task по репозиторию задачи (ORCH / Sandbox);
|
||||
- `issue_id` (UUID) — из БД: колонка `tasks.plane_issue_id`.
|
||||
- Рендер через `<a href="...">ORCH-NNN</a>` (`parse_mode=HTML` уже включён);
|
||||
HTML в title/тексте экранируется, чтобы не сломать разметку.
|
||||
|
||||
### 4.4. Требование 4 — кликабельный номер во ВСЕХ уведомлениях орка
|
||||
- Единый хелпер (напр. `plane_issue_link(work_item_id, plane_issue_id, project_id) -> html`)
|
||||
строит кликабельный номер с fail-safe; применяется во всех точках `send_telegram`/
|
||||
`notify_*`, где упоминается `work_item_id` (approve-requested, QG-fail, deploy
|
||||
SUCCESS/FAIL, Needs Input, прод-деплой, alert'ы launcher/merge_gate/job_reaper/
|
||||
security_gate/reconciler/main).
|
||||
|
||||
## 5. Вне объёма (out of scope)
|
||||
|
||||
- Транспорт `send_telegram` / `edit_telegram` / `delete_telegram` (parse_mode HTML уже есть) — не трогать.
|
||||
- Инвариант «одна карточка на задачу» — не нарушать (не плодить дубли).
|
||||
- Логика `disable_notification` (карточка тихая; пингуют только alert-хелперы) — не трогать.
|
||||
- `STAGE_TRANSITIONS`, Quality Gates, схема БД — НЕ менять.
|
||||
- Изменение поведения агентов/конвейера.
|
||||
|
||||
## 6. Зависимости
|
||||
|
||||
- Маппинг статусов (требование 2) опирается на статусную модель ORCH-066. ORCH-066 уже в
|
||||
конвейере на стадии deploy. Эту задачу делать ПОСЛЕ прода ORCH-066, чтобы имена статусов
|
||||
совпали. Если ORCH-066 ещё не в проде на момент разработки — использовать согласованные
|
||||
финальные имена из модели: To Analyse, Analysis, Code-Review, Awaiting Deploy, Deploying,
|
||||
Monitoring after Deploy, In Review, Needs Input, Blocked, Cancelled, Done.
|
||||
- Конфигурация `plane_web_url` / `plane_workspace_slug` уже существует в `src/config.py`
|
||||
(ORCH-017); реестр проектов `src/projects.py` (`get_project_by_repo().plane_project_id`)
|
||||
уже даёт per-task project_id.
|
||||
|
||||
## 7. Fail-safe (обязательно)
|
||||
|
||||
- Нет `PLANE_WEB_BASE` / нет `plane_issue_id` / нет `project_id` / loopback-база →
|
||||
показывать номер БЕЗ ссылки, **не падать**.
|
||||
- HTML-экранирование пользовательского текста (title и пр.) во всех сообщениях с
|
||||
`parse_mode=HTML`.
|
||||
- Bump: транзиентный фейл `send` не обнуляет `tracker_message_id` и не плодит дубли.
|
||||
- Любая ошибка построения статуса/ссылки никогда не должна ронять рендер карточки или
|
||||
отправку уведомления (degrade gracefully).
|
||||
|
||||
## 8. Критерии успеха (Definition of Done)
|
||||
|
||||
- Bump работает из коробки: карточка падает вниз при обновлении, одна на задачу.
|
||||
- Карточка показывает Plane-статус новой модели, включая `⏸️ In Review` (согласование BRD),
|
||||
`⏸️ Awaiting Deploy`, `❓ Needs Input`.
|
||||
- Номер задачи кликабелен в карточке И во всех уведомлениях орка (ведёт на страницу Plane).
|
||||
- Fail-safe покрыт тестами (нет URL/plane_id/project → без ссылки, не падает;
|
||||
HTML-экранирование).
|
||||
- `pytest tests/ -q` зелёный.
|
||||
- Документация обновлена в том же PR: `CLAUDE.md` (раздел нотификаций/tracker),
|
||||
`CHANGELOG.md`, ADR per-work-item.
|
||||
|
||||
## 9. Риски
|
||||
|
||||
- **Регресс enduro-trails.** Смена дефолта `tracker_mode` на bump меняет поведение для всех
|
||||
проектов. Митигация: bump уже реализован и протестирован концептуально; инвариант «одна
|
||||
карточка» сохранён; env-переключатель `edit` остаётся.
|
||||
- **Поломка HTML-разметки** при неэкранированном title → сообщение не доставится. Митигация:
|
||||
обязательное `html.escape` + тесты.
|
||||
- **Источник «истинного» Plane-статуса** для веток, не выводимых из `tasks.stage`
|
||||
(Needs Input/Blocked/Rejected/Cancelled, Deploying/Monitoring), при запрете на изменение
|
||||
схемы БД — архитектурное решение (ADR), с обязательным fail-safe (без сети не падать).
|
||||
- **Self-hosting.** Орк правит сам себя; обязательна страховка через staging (8501) перед
|
||||
прод-деплоем; прод-контейнер не ронять в рамках задачи.
|
||||
205
docs/work-items/ORCH-067/02-trz.md
Normal file
205
docs/work-items/ORCH-067/02-trz.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# ТЗ — ORCH-067: Telegram tracker (bump + статусы Plane + кликабельный номер задачи)
|
||||
|
||||
Work Item: **ORCH-067**
|
||||
Документ описывает КОНКРЕТНЫЕ изменения кода/конфигурации/тестов и документации.
|
||||
Архитектурные развилки помечены `[ARCH]` — решение принимает архитектор (ADR), здесь
|
||||
зафиксированы только требования и ограничения к ним.
|
||||
|
||||
---
|
||||
|
||||
## 0. Задействованные модули `src/`
|
||||
|
||||
| Модуль | Роль в задаче |
|
||||
|---|---|
|
||||
| `src/config.py` | Дефолт `tracker_mode`; поле `plane_web_url`/`plane_workspace_slug` (уже есть). |
|
||||
| `src/notifications.py` | Основные изменения: bump-дефолт, статус-строка карточки, хелпер ссылки, применение хелпера в `notify_*`. |
|
||||
| `src/plane_sync.py` | Источник имён статусов/маппинга ORCH-066 (`_PLANE_NAME_TO_KEY`, `_STAGE_TO_STATE_KEY`); при необходимости reverse-map UUID→имя `[ARCH]`. |
|
||||
| `src/projects.py` | `get_project_by_repo(repo).plane_project_id` — per-task project_id для ссылки. |
|
||||
| `src/db.py` | Чтение `tasks.plane_issue_id`, `tasks.repo` (без изменений схемы). |
|
||||
| `src/stage_engine.py`, `src/agents/launcher.py`, `src/merge_gate.py`, `src/job_reaper.py`, `src/security_gate.py`, `src/reconciler.py`, `src/main.py` | Точки `send_telegram`, где есть `work_item_id` — применить хелпер ссылки (требование 4). |
|
||||
|
||||
Изменения API (HTTP endpoints) — **нет**. Изменения схемы БД — **нет**. Новые QG checks — **нет**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Требование 1 — bump по умолчанию
|
||||
|
||||
### 1.1. Изменение
|
||||
- `src/config.py` (~стр. 408): сменить дефолт
|
||||
`tracker_mode: str = "edit"` → `tracker_mode: str = "bump"`.
|
||||
- Обновить docstring-комментарий рядом (ORCH-042): отметить, что **дефолт теперь `bump`**,
|
||||
`edit` остаётся доступен через `ORCH_TRACKER_MODE=edit`.
|
||||
|
||||
### 1.2. Без изменений (сохранить инвариант)
|
||||
- Логика `update_task_tracker` (`src/notifications.py`, ветка `if mode == "bump"`):
|
||||
`delete_telegram(old)` best-effort → `send_telegram(text, disable_notification=True)` →
|
||||
`set_tracker_message_id` ТОЛЬКО при `new_mid is not None`. Не менять.
|
||||
- `send_telegram`/`edit_telegram`/`delete_telegram` — не трогать.
|
||||
|
||||
### 1.3. Прод-аспект
|
||||
- Для прод-инстанса орка можно дополнительно выставить `ORCH_TRACKER_MODE=bump` в `.env`
|
||||
на хосте (как страховку), но код должен работать «из коробки» и без env. Канон env —
|
||||
`.env.example` (обновить, если там фигурирует tracker_mode).
|
||||
|
||||
---
|
||||
|
||||
## 2. Требование 2 — статус-строка карточки по модели ORCH-066
|
||||
|
||||
### 2.1. Новый чистый хелпер маппинга
|
||||
Добавить в `src/notifications.py` функцию, возвращающую отображаемый Plane-статус для
|
||||
карточки на основе доступных данных задачи. Сигнатура (ориентир):
|
||||
```python
|
||||
def plane_status_label(task_row) -> str:
|
||||
"""Вернуть строку текущего Plane-статуса для шапки карточки (с emoji).
|
||||
Никогда не падает: на неизвестном входе -> разумный дефолт по stage."""
|
||||
```
|
||||
Хелпер обязан быть чистым/детерминированным от входных данных и **никогда не бросать**
|
||||
исключения (любая ошибка → дефолт по `stage`, рендер карточки не ломается).
|
||||
|
||||
### 2.2. Маппинг внутреннее состояние → Plane-статус (обязательные строки)
|
||||
Имена статусов — финальные из модели ORCH-066 (см. `_PLANE_NAME_TO_KEY` в `plane_sync.py`).
|
||||
|
||||
| Источник (данные задачи в БД) | Plane-статус (отображение в карточке) |
|
||||
|---|---|
|
||||
| `stage == "created"` | `To Analyse` |
|
||||
| `stage == "analysis"`, BRD-clock не запущен | `Analysis` |
|
||||
| `stage == "analysis"`, `brd_review_started_at` есть, `brd_review_ended_at` пуст | `⏸️ In Review — ожидание согласования BRD` |
|
||||
| `stage == "architecture"` | `Architecture` |
|
||||
| `stage == "development"` | `Development` |
|
||||
| `stage == "review"` | `Code-Review` |
|
||||
| `stage == "testing"` | `Testing` |
|
||||
| `stage == "deploy"` (ожидание Confirm Deploy) | `⏸️ Awaiting Deploy — ожидание Confirm Deploy` |
|
||||
| `stage == "done"` | `Done` |
|
||||
|
||||
Ветки (Needs Input / Blocked / Rejected / Cancelled / Deploying / Monitoring after Deploy):
|
||||
- `❓ Needs Input — нужны уточнения` — состояние «аналитик задал вопросы»;
|
||||
- `Blocked`, `Rejected`, `Cancelled`, `Deploying`, `Monitoring after Deploy`.
|
||||
|
||||
`[ARCH]` **Источник сигнала для веток, не выводимых из `tasks.stage`** (Needs Input,
|
||||
Blocked, Rejected, Cancelled, Deploying, Monitoring after Deploy):
|
||||
- запрещено менять схему БД (нельзя добавлять колонку-флаг);
|
||||
- варианты для архитектора: (а) best-effort чтение живого Plane-статуса
|
||||
(`fetch_issue_state` + reverse-map UUID→имя через `get_project_states`/
|
||||
`_PLANE_NAME_TO_KEY`) с обязательным fail-safe (нет сети/ответа → деградация на
|
||||
stage-маппинг, без задержки, блокирующей конвейер); (б) только stage-выводимые статусы,
|
||||
а ветки — по уже имеющимся сигналам (например, In Review через brd-clock).
|
||||
- ОБЯЗАТЕЛЬНО к покрытию (DoD): `⏸️ In Review`, `⏸️ Awaiting Deploy`, `❓ Needs Input`.
|
||||
In Review полностью выводится из brd-clock (см. таблицу) и должен работать без сети.
|
||||
|
||||
### 2.3. Встраивание в `render_task_tracker`
|
||||
- В `render_task_tracker` (`src/notifications.py`) добавить в шапку/верх карточки отдельную
|
||||
СТРОКУ статуса (под заголовком `🛠️ ORCH-NNN · <title>` / над разделителем `bar`),
|
||||
напр.: `📍 <status_label>`.
|
||||
- Существующие строки по стадиям (`✅ done` / `🔄 active`), строка «Подтверждение BRD»,
|
||||
тоталы токенов/стоимости, done-строка с PR/⏱️ — СОХРАНИТЬ (семантику не ломать).
|
||||
- Семантика строки «Подтверждение BRD» (⏸️+⏳ при ожидании, ✅ при пройденном гейте)
|
||||
сохраняется; новая статус-строка дублирует её смысл в терминах Plane-статуса.
|
||||
|
||||
---
|
||||
|
||||
## 3. Требование 3 + 4 — кликабельный номер задачи
|
||||
|
||||
### 3.1. Единый хелпер
|
||||
Добавить в `src/notifications.py`:
|
||||
```python
|
||||
def plane_issue_link(work_item_id, plane_issue_id=None, project_id=None, repo=None) -> str:
|
||||
"""Вернуть HTML с кликабельным номером задачи (<a href=...>ORCH-NNN</a>),
|
||||
либо просто html.escape(work_item_id), если ссылку построить нельзя.
|
||||
Никогда не падает."""
|
||||
```
|
||||
Поведение:
|
||||
- База URL: `settings.plane_web_url` → fallback `settings.plane_api_url`; loopback-база
|
||||
(`localhost`/`127.0.0.1`/…) трактуется как «нет web URL» (переиспользовать
|
||||
`_is_loopback_base`).
|
||||
- `workspace_slug`: `settings.plane_workspace_slug`.
|
||||
- `project_id`: явный аргумент → иначе резолв по `repo` через
|
||||
`get_project_by_repo(repo).plane_project_id`.
|
||||
- `issue_id`: `plane_issue_id` (UUID из `tasks.plane_issue_id`).
|
||||
- URL-шаблон: `{web_base}/{workspace}/projects/{project_id}/issues/{issue_id}/`.
|
||||
- Текст ссылки = `html.escape(work_item_id)`; `href` = `html.escape(url, quote=True)`.
|
||||
- **Fail-safe:** если не хватает любого из (`web_base` валидный/не loopback, `workspace`,
|
||||
`project_id`, `plane_issue_id`) → вернуть `html.escape(work_item_id)` (номер без ссылки).
|
||||
- Логика построения URL уже существует в `_build_plane_issue_link` (ORCH-017) — допустимо
|
||||
переиспользовать/обобщить её, разнеся «текст-ссылки = номер» и «текст-ссылки = `✅ Задача
|
||||
в Plane`», чтобы не дублировать резолв проекта и loopback-guard.
|
||||
|
||||
### 3.2. Применение в карточке (требование 3)
|
||||
- В `render_task_tracker` заголовок строится из `work_item_id`. Заменить
|
||||
`html.escape(work_item_id)` в обоих вариантах заголовка (done / not-done) на
|
||||
`plane_issue_link(work_item_id, plane_issue_id, repo=repo)` — номер становится
|
||||
кликабельным.
|
||||
- Для этого `render_task_tracker` должен дополнительно выбрать из БД `repo` и
|
||||
`plane_issue_id` (расширить существующий `SELECT` по `tasks`). Схему НЕ менять — колонки
|
||||
уже есть.
|
||||
- `title` уже экранируется (`html.escape(title)`) — сохранить.
|
||||
|
||||
### 3.3. Применение во всех уведомлениях (требование 4)
|
||||
Во всех точках `send_telegram`/`notify_*`, где в тексте есть `work_item_id`, заменить
|
||||
«сырой» номер на `plane_issue_link(...)`. Перечень точек (из `src`):
|
||||
- `src/notifications.py`: `notify_approve_requested`, `notify_error`
|
||||
(и любые будущие notify_* с work_item_id);
|
||||
- `src/stage_engine.py`: все `send_telegram(...)` с `work_item_id`
|
||||
(≈ строки 613, 672, 719, 776, 820, 916, 971, 1057, 1134, 1192, 1228, 1257, 1355, 1367,
|
||||
1425, 1447, 1601 — проверить каждую: применять ТОЛЬКО где упоминается номер задачи);
|
||||
- `src/agents/launcher.py`: deploy-failed alert (≈685–686), agent-failed alert (≈698–699),
|
||||
alert ≈821–822;
|
||||
- `src/merge_gate.py` (≈431–432);
|
||||
- `src/job_reaper.py` (≈395–396);
|
||||
- `src/security_gate.py` (≈673–674);
|
||||
- `src/reconciler.py` (≈449);
|
||||
- `src/main.py` (≈45–47).
|
||||
|
||||
`[ARCH]` Способ доступа к `plane_issue_id`/`project_id` в каждой точке (часто там уже есть
|
||||
`work_item_id`, но не обязательно `plane_issue_id`): хелпер должен уметь резолвить
|
||||
недостающее по `repo`/БД, оставаясь fail-safe. Допустимо добавить тонкую обёртку, которая по
|
||||
`work_item_id`/`task_id` достаёт `repo`+`plane_issue_id` из БД и зовёт `plane_issue_link`
|
||||
(аналогично существующему `_get_task_link_fields`). Везде, где данных нет — деградация на
|
||||
просто номер, без падения.
|
||||
|
||||
### 3.4. HTML-экранирование
|
||||
- `parse_mode=HTML` уже стоит в `send_telegram`/`edit_telegram`. Любой пользовательский
|
||||
текст (title, описания, причины QG-fail, сообщения об ошибках), попадающий в сообщение с
|
||||
ссылками, должен экранироваться `html.escape`, чтобы не сломать `<a>`-разметку.
|
||||
|
||||
---
|
||||
|
||||
## 4. Конфигурация
|
||||
|
||||
- `plane_web_url` (env `ORCH_PLANE_WEB_URL`) — уже существует (`src/config.py`), значение
|
||||
прод — `plane.mva154.duckdns.org` (схему `https://` учесть при сборке URL).
|
||||
Дополнительных полей конфигурации не требуется.
|
||||
- `tracker_mode` — сменить дефолт на `bump` (раздел 1).
|
||||
- Обновить `.env.example`, если в нём фигурируют `ORCH_TRACKER_MODE` / `ORCH_PLANE_WEB_URL`
|
||||
(канон секретов/настроек — `.env.example`, не коммитить реальные секреты).
|
||||
|
||||
---
|
||||
|
||||
## 5. Артефакты pipeline, которые должны быть созданы/обновлены
|
||||
|
||||
- `docs/work-items/ORCH-067/06-adr/ADR-NNN-*.md` — архитектурное решение (минимум: источник
|
||||
«истинного» Plane-статуса для веток при запрете изменения схемы БД; дефолт bump; единый
|
||||
хелпер ссылки).
|
||||
- `CLAUDE.md` — раздел про нотификации/tracker (дефолт bump; статус-строка карточки;
|
||||
кликабельный номер в карточке и уведомлениях).
|
||||
- `CHANGELOG.md` — запись ORCH-067.
|
||||
- `docs/architecture/README.md` — при необходимости синхронизировать описание tracker'а.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ограничения (что НЕ трогать)
|
||||
|
||||
- Транспорт `send_telegram`/`edit_telegram`/`delete_telegram`.
|
||||
- Инвариант «одна карточка на задачу».
|
||||
- Логику `disable_notification` (карточка тихая; пингуют только alert-хелперы).
|
||||
- `STAGE_TRANSITIONS`, Quality Gates, схему БД.
|
||||
- Поведение агентов/конвейера.
|
||||
|
||||
---
|
||||
|
||||
## 7. Замечания по самохостингу
|
||||
|
||||
Орк правит сам себя в проде (общий инстанс/БД с enduro-trails):
|
||||
- НЕ перезапускать прод-контейнер `orchestrator` в рамках задачи.
|
||||
- Обязательная страховка через `deploy-staging` (8501) до прод-деплоя.
|
||||
- Смена дефолта `tracker_mode` затрагивает ВСЕ проекты — проверить отсутствие регресса для
|
||||
enduro-trails (тесты + staging-наблюдение карточки).
|
||||
129
docs/work-items/ORCH-067/03-acceptance-criteria.md
Normal file
129
docs/work-items/ORCH-067/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Acceptance Criteria — ORCH-067
|
||||
|
||||
Work Item: **ORCH-067**
|
||||
Каждый критерий формулирует чёткое условие PASS/FAIL. Привязка к тестам — в `04-test-plan.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## Группа A — Bump по умолчанию (Требование 1)
|
||||
|
||||
### AC-1 — дефолт tracker_mode = bump
|
||||
- **PASS:** `Settings().tracker_mode == "bump"` без выставленного env `ORCH_TRACKER_MODE`.
|
||||
- **FAIL:** дефолт остался `"edit"` или иное.
|
||||
|
||||
### AC-2 — bump-поведение: одна карточка падает вниз
|
||||
- **PASS:** при втором (и последующем) вызове `update_task_tracker` для задачи с уже
|
||||
сохранённым `tracker_message_id` вызывается `delete_telegram(old_id)` (best-effort),
|
||||
затем `send_telegram(...)` с `disable_notification=True`, затем `set_tracker_message_id`
|
||||
на новый id. В чате остаётся ровно одна карточка на задачу.
|
||||
- **FAIL:** карточка редактируется на месте при дефолте; либо появляются дубли; либо новая
|
||||
карточка отправляется со звуком (`disable_notification` не True).
|
||||
|
||||
### AC-3 — bump fail-safe: транзиентный фейл send не обнуляет указатель
|
||||
- **PASS:** если `send_telegram` вернул `None` (нет креды/транзиентный фейл),
|
||||
`tracker_message_id` НЕ перезаписывается в `None` и дубликат в рамках вызова не создаётся.
|
||||
- **FAIL:** указатель обнулён или создан второй card-месседж в одном вызове.
|
||||
|
||||
### AC-4 — режим edit остаётся доступен через env
|
||||
- **PASS:** при `ORCH_TRACKER_MODE=edit` поведение прежнее (editMessageText, fallback на
|
||||
новый месседж только при EDIT_GONE).
|
||||
- **FAIL:** edit-режим сломан/недоступен.
|
||||
|
||||
---
|
||||
|
||||
## Группа B — Статус-строка карточки по модели ORCH-066 (Требование 2)
|
||||
|
||||
### AC-5 — статус-строка присутствует в карточке
|
||||
- **PASS:** `render_task_tracker(task_id)` содержит явную строку текущего Plane-статуса
|
||||
(напр. `📍 <status>`) в шапке/верхней части карточки.
|
||||
- **FAIL:** статус-строки нет.
|
||||
|
||||
### AC-6 — корректный маппинг stage → Plane-статус
|
||||
- **PASS:** для всех stage-выводимых состояний строка статуса соответствует таблице ТЗ §2.2:
|
||||
`created→To Analyse`, `analysis→Analysis`, `architecture→Architecture`,
|
||||
`development→Development`, `review→Code-Review`, `testing→Testing`,
|
||||
`deploy→Awaiting Deploy`, `done→Done`.
|
||||
- **FAIL:** хотя бы один stage маппится на неверное имя/внутренний ярлык.
|
||||
|
||||
### AC-7 — In Review (ожидание согласования BRD) как полноценный статус
|
||||
- **PASS:** при `stage == "analysis"`, `brd_review_started_at` задан и
|
||||
`brd_review_ended_at` пуст — статус-строка явно отражает `⏸️ In Review` с пометкой
|
||||
«ожидание согласования BRD»; при этом существующая строка «Подтверждение BRD …» с ⏸️/⏳
|
||||
сохранена. Работает без сетевых вызовов.
|
||||
- **FAIL:** In Review теряется/не показан как статус, либо строка «Подтверждение BRD» исчезла.
|
||||
|
||||
### AC-8 — Awaiting Deploy и Needs Input отражены
|
||||
- **PASS:** состояние ожидания Confirm Deploy показывается как
|
||||
`⏸️ Awaiting Deploy — ожидание Confirm Deploy`; состояние вопросов аналитика — как
|
||||
`❓ Needs Input — нужны уточнения`.
|
||||
- **FAIL:** любое из этих состояний не отражено в статус-строке.
|
||||
|
||||
### AC-9 — рендер карточки никогда не падает
|
||||
- **PASS:** при любой ошибке построения статуса (битые данные, недоступный источник)
|
||||
`render_task_tracker` возвращает корректную карточку (деградация на stage-маппинг или
|
||||
fallback-строку), исключение наружу не выходит.
|
||||
- **FAIL:** `render_task_tracker` бросает исключение.
|
||||
|
||||
---
|
||||
|
||||
## Группа C — Кликабельный номер в карточке (Требование 3)
|
||||
|
||||
### AC-10 — номер задачи в карточке — гиперссылка
|
||||
- **PASS:** при наличии `plane_web_url` (не loopback), `plane_workspace_slug`, `project_id`
|
||||
(резолв по repo) и `plane_issue_id` карточка содержит
|
||||
`<a href="https://<base>/<ws>/projects/<pid>/issues/<issue_id>/">ORCH-NNN</a>`.
|
||||
- **FAIL:** номер выводится сырым текстом при наличии всех данных, либо URL собран неверно.
|
||||
|
||||
### AC-11 — fail-safe ссылки в карточке
|
||||
- **PASS:** при отсутствии любого из (web_base/не-loopback, workspace, project_id,
|
||||
plane_issue_id) карточка показывает номер БЕЗ ссылки (`html.escape(work_item_id)`) и не
|
||||
падает.
|
||||
- **FAIL:** падение, пустая ссылка `<a href="">`, либо битый `<a>` тег.
|
||||
|
||||
---
|
||||
|
||||
## Группа D — Кликабельный номер во всех уведомлениях (Требование 4)
|
||||
|
||||
### AC-12 — единый хелпер ссылки
|
||||
- **PASS:** существует `plane_issue_link(...)`, возвращающий HTML-ссылку при достаточных
|
||||
данных и `html.escape(work_item_id)` при недостаточных; никогда не бросает.
|
||||
- **FAIL:** хелпера нет, либо он падает на неполных данных.
|
||||
|
||||
### AC-13 — хелпер применён во всех уведомлениях с work_item_id
|
||||
- **PASS:** во всех точках `send_telegram`/`notify_*` из ТЗ §3.3, где упоминается
|
||||
`work_item_id` (`notify_approve_requested`, `notify_error`, alert'ы stage_engine,
|
||||
launcher, merge_gate, job_reaper, security_gate, reconciler, main), номер задачи
|
||||
кликабелен (при наличии данных) и ведёт на ту же страницу Plane.
|
||||
- **FAIL:** хотя бы одна такая точка выводит номер сырым текстом при наличии данных.
|
||||
|
||||
### AC-14 — HTML-экранирование пользовательского текста
|
||||
- **PASS:** title/причины/сообщения с потенциальным HTML (`<`, `>`, `&`) экранируются
|
||||
`html.escape`; разметка `<a>` остаётся валидной; сообщение проходит `parse_mode=HTML`.
|
||||
- **FAIL:** неэкранированный текст ломает разметку (тест с title, содержащим `<b>`/`&`,
|
||||
обнаруживает поломку).
|
||||
|
||||
---
|
||||
|
||||
## Группа E — Нерегресс и качество
|
||||
|
||||
### AC-15 — инварианты транспорта/нотификаций сохранены
|
||||
- **PASS:** `send_telegram`/`edit_telegram`/`delete_telegram` не изменены по сигнатуре/
|
||||
семантике; карточка тихая (`disable_notification=True`); инвариант «одна карточка на
|
||||
задачу» соблюдён; `STAGE_TRANSITIONS`/QG/схема БД не тронуты.
|
||||
- **FAIL:** изменён транспорт, карточка пингует, появились дубли, тронута схема БД/QG.
|
||||
|
||||
### AC-16 — нет регресса для enduro-trails
|
||||
- **PASS:** существующие тесты нотификаций (`test_notify_approve_links.py`,
|
||||
`test_notify_done_regression.py` и др.) проходят; поведение карточки для не-ORCH проектов
|
||||
без новых Plane-статусов деградирует корректно (alias-fallback, без ссылки при нехватке
|
||||
данных).
|
||||
- **FAIL:** падение существующих тестов или сломанная карточка для enduro.
|
||||
|
||||
### AC-17 — весь набор тестов зелёный
|
||||
- **PASS:** `pytest tests/ -q` зелёный.
|
||||
- **FAIL:** любой упавший тест.
|
||||
|
||||
### AC-18 — документация обновлена в том же PR
|
||||
- **PASS:** обновлены `CLAUDE.md` (раздел нотификаций/tracker), `CHANGELOG.md`,
|
||||
создан ADR per-work-item.
|
||||
- **FAIL:** функционал изменён, документация — нет (reviewer → REQUEST_CHANGES).
|
||||
181
docs/work-items/ORCH-067/04-test-plan.yaml
Normal file
181
docs/work-items/ORCH-067/04-test-plan.yaml
Normal file
@@ -0,0 +1,181 @@
|
||||
work_item: ORCH-067
|
||||
description: >
|
||||
План тестов для ORCH-067 (Telegram tracker: bump по умолчанию, статус-строка
|
||||
карточки по модели Plane ORCH-066, кликабельный номер задачи в карточке и во
|
||||
всех уведомлениях орка). Сеть изолируется: send_telegram/edit_telegram/
|
||||
delete_telegram подменяются рекордерами (как в tests/conftest.py и
|
||||
tests/test_notify_approve_links.py); БД — временный SQLite, сидируемый фикстурой.
|
||||
|
||||
tests:
|
||||
# --- Группа A: bump по умолчанию (AC-1..AC-4) ---
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "Дефолт Settings().tracker_mode == 'bump' без env ORCH_TRACKER_MODE"
|
||||
module: tests/test_tracker_bump_default.py
|
||||
asserts: "AC-1"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
bump-поведение: при повторном update_task_tracker с сохранённым
|
||||
tracker_message_id вызывается delete_telegram(old) -> send_telegram(...,
|
||||
disable_notification=True) -> set_tracker_message_id(new). Одна карточка.
|
||||
module: tests/test_tracker_bump_default.py
|
||||
asserts: "AC-2"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
bump fail-safe: send_telegram вернул None (нет креды/транзиент) ->
|
||||
tracker_message_id не обнуляется, дубликат в вызове не создаётся.
|
||||
module: tests/test_tracker_bump_default.py
|
||||
asserts: "AC-3"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "ORCH_TRACKER_MODE=edit -> прежнее edit-поведение (editMessageText)"
|
||||
module: tests/test_tracker_bump_default.py
|
||||
asserts: "AC-4"
|
||||
expected: PASS
|
||||
|
||||
# --- Группа B: статус-строка карточки (AC-5..AC-9) ---
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "render_task_tracker содержит явную строку текущего Plane-статуса"
|
||||
module: tests/test_tracker_status_line.py
|
||||
asserts: "AC-5"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: >
|
||||
Маппинг stage -> Plane-статус по таблице ТЗ §2.2: created->To Analyse,
|
||||
analysis->Analysis, architecture->Architecture, development->Development,
|
||||
review->Code-Review, testing->Testing, deploy->Awaiting Deploy, done->Done
|
||||
(параметризованный тест по всем stage).
|
||||
module: tests/test_tracker_status_line.py
|
||||
asserts: "AC-6"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: >
|
||||
analysis + brd_review_started_at задан + brd_review_ended_at пуст ->
|
||||
статус '⏸️ In Review' (ожидание согласования BRD); строка 'Подтверждение
|
||||
BRD' с ⏸️/⏳ сохранена; без сетевых вызовов.
|
||||
module: tests/test_tracker_status_line.py
|
||||
asserts: "AC-7"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: >
|
||||
Awaiting Deploy ('ожидание Confirm Deploy') и Needs Input ('нужны
|
||||
уточнения') корректно отражаются в статус-строке.
|
||||
module: tests/test_tracker_status_line.py
|
||||
asserts: "AC-8"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: >
|
||||
render_task_tracker не падает при битых/недоступных данных статуса
|
||||
(деградация на stage-маппинг/fallback, исключение не наружу).
|
||||
module: tests/test_tracker_status_line.py
|
||||
asserts: "AC-9, AC-16"
|
||||
expected: PASS
|
||||
|
||||
# --- Группа C: кликабельный номер в карточке (AC-10..AC-11) ---
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: >
|
||||
При полных данных (plane_web_url не loopback, workspace, project_id по repo,
|
||||
plane_issue_id) карточка содержит <a href=".../issues/<id>/">ORCH-NNN</a>
|
||||
с корректным URL.
|
||||
module: tests/test_tracker_issue_link.py
|
||||
asserts: "AC-10"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: >
|
||||
Fail-safe ссылки в карточке: при отсутствии любого из (web_base/не-loopback,
|
||||
workspace, project_id, plane_issue_id) номер выводится html.escape без <a>,
|
||||
рендер не падает. Параметризовать по каждому отсутствующему полю.
|
||||
module: tests/test_tracker_issue_link.py
|
||||
asserts: "AC-11"
|
||||
expected: PASS
|
||||
|
||||
# --- Группа D: единый хелпер и уведомления (AC-12..AC-14) ---
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: >
|
||||
plane_issue_link(...) возвращает HTML-ссылку при достаточных данных и
|
||||
html.escape(work_item_id) при недостаточных; никогда не бросает (в т.ч. на
|
||||
None-аргументах и loopback-базе).
|
||||
module: tests/test_plane_issue_link.py
|
||||
asserts: "AC-12"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: >
|
||||
notify_approve_requested: номер задачи кликабелен (ведёт на страницу Plane),
|
||||
сохранён call-to-action 'Approved', ровно одно notifying-сообщение.
|
||||
module: tests/test_notify_issue_links.py
|
||||
asserts: "AC-13"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: >
|
||||
notify_error: номер задачи кликабелен при наличии данных, деградирует на
|
||||
сырой номер без падения при их отсутствии.
|
||||
module: tests/test_notify_issue_links.py
|
||||
asserts: "AC-13, AC-12"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: integration
|
||||
description: >
|
||||
Точки send_telegram в stage_engine/launcher/merge_gate/job_reaper/
|
||||
security_gate/reconciler/main, где есть work_item_id, используют
|
||||
plane_issue_link (или эквивалент) — номер кликабелен. Проверка рекордером
|
||||
send_telegram на представительных alert-путях (deploy fail, agent fail,
|
||||
QG fail, прод-деплой).
|
||||
module: tests/test_notify_issue_links.py
|
||||
asserts: "AC-13"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: >
|
||||
HTML-экранирование: title с '<b>'/'&'/'>' экранируется, <a>-разметка
|
||||
остаётся валидной, сообщение не ломается под parse_mode=HTML (карточка и
|
||||
уведомления).
|
||||
module: tests/test_tracker_issue_link.py
|
||||
asserts: "AC-14"
|
||||
expected: PASS
|
||||
|
||||
# --- Группа E: нерегресс (AC-15..AC-18) ---
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: >
|
||||
Инварианты: карточка отправляется с disable_notification=True; одна карточка
|
||||
на задачу; транспорт send/edit/delete не изменён по семантике.
|
||||
module: tests/test_tracker_bump_default.py
|
||||
asserts: "AC-15"
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: >
|
||||
Нерегресс существующих тестов нотификаций (test_notify_approve_links.py,
|
||||
test_notify_done_regression.py) и корректная деградация карточки для
|
||||
enduro-trails без новых Plane-статусов.
|
||||
module: tests/test_notify_done_regression.py
|
||||
asserts: "AC-16, AC-17"
|
||||
expected: PASS
|
||||
@@ -0,0 +1,224 @@
|
||||
# ADR-001: Источник Plane-статуса для live-карточки и кликабельный номер задачи
|
||||
|
||||
- **Статус:** Proposed
|
||||
- **Дата:** 2026-06-08
|
||||
- **Задача:** ORCH-067
|
||||
- **Слой:** B (индикация), НЕ слой A (машина стадий) — см. CLAUDE.md / ORCH-066
|
||||
- **Связи:** ORCH-066 (статусная модель Plane, `_PLANE_NAME_TO_KEY` / `_STAGE_TO_STATE_KEY`),
|
||||
ORCH-042 (live-tracker, режимы `edit`/`bump`), ORCH-017 (`_build_plane_issue_link`,
|
||||
`plane_web_url`/`plane_workspace_slug`, loopback-guard), ORCH-059 (Confirm Deploy),
|
||||
ORCH-060 (`fetch_issue_state`), ORCH-010 (`get_project_states` per-project + кэш),
|
||||
adr-0001 (реестр проектов), adr-0010 (post-deploy monitor).
|
||||
|
||||
## Контекст
|
||||
|
||||
ТЗ ORCH-067 (`02-trz.md`) фиксирует объём изменений; данный ADR закрывает развилки,
|
||||
явно отданные архитектору метками `[ARCH]`:
|
||||
|
||||
1. **Источник «истинного» Plane-статуса для веток, не выводимых из `tasks.stage`**
|
||||
(Needs Input, Blocked, Rejected, Cancelled, Deploying, Monitoring after Deploy),
|
||||
при **запрете менять схему БД** (нельзя добавить колонку-флаг). TZ §2.2 предлагает
|
||||
два варианта: (а) best-effort чтение живого Plane-статуса с fail-safe;
|
||||
(б) только stage-выводимые статусы.
|
||||
2. **Способ доступа к `plane_issue_id`/`project_id`** в каждой точке `send_telegram`,
|
||||
где есть только `work_item_id` (требование 4), оставаясь fail-safe.
|
||||
3. Смена дефолта `tracker_mode` (`edit` → `bump`) для общего инстанса.
|
||||
|
||||
### Ключевая находка анализа (определяет развилку 1)
|
||||
|
||||
Когда аналитик задаёт вопросы, `stage_engine.start_pipeline` при наличии
|
||||
`01-questions.md` вызывает `set_issue_needs_input(work_item_id)` (Plane → Needs Input),
|
||||
но **DB-стадия остаётся `analysis`**, а BRD-часы (`brd_review_started_at`) **не
|
||||
запускаются** (они стартуют позже, в `notify_approve_requested`, когда BRD готов).
|
||||
Следовательно состояния **`Analysis` (аналитик работает)** и **`❓ Needs Input`
|
||||
(аналитик ждёт ответа)** **неразличимы** по offline-данным БД (`stage` + brd-clock).
|
||||
Единственный авторитетный источник этого различия — **живой Plane-статус**, который
|
||||
оркестратор сам выставил через `set_issue_needs_input`.
|
||||
|
||||
То же касается `Deploying` / `Monitoring after Deploy`: на стадии `deploy`/`done`
|
||||
конкретная фаза self-deploy видна только в Plane (ORCH-059/ORCH-066), не в `tasks.stage`.
|
||||
|
||||
Вывод: чисто-offline вариант (б) **не покрывает обязательный по DoD `❓ Needs Input`**
|
||||
(AC-8). Нужен гибрид.
|
||||
|
||||
## Решение
|
||||
|
||||
### Р-1. Гибрид: offline-first ядро + best-effort live-overlay
|
||||
|
||||
Статус карточки строится в два слоя; **offline-ядро авторитетно и всегда работает без
|
||||
сети**, live-overlay лишь дорисовывает ветки, неотличимые offline.
|
||||
|
||||
**Слой 1 — чистая offline-функция `plane_status_label(task_row) -> str`** в
|
||||
`src/notifications.py`. Детерминированная, **никогда не бросает**, **никогда не ходит в
|
||||
сеть**. Маппинг (имена статусов — финальные из ORCH-066 `_PLANE_NAME_TO_KEY`):
|
||||
|
||||
| Источник (DB) | Метка карточки |
|
||||
|---|---|
|
||||
| `stage == "created"` | `To Analyse` |
|
||||
| `stage == "analysis"`, brd-clock не запущен | `Analysis` |
|
||||
| `stage == "analysis"`, `brd_review_started_at` есть, `brd_review_ended_at` пуст | `⏸️ In Review — ожидание согласования BRD` |
|
||||
| `stage == "architecture"` | `Architecture` |
|
||||
| `stage == "development"` | `Development` |
|
||||
| `stage == "review"` | `Code-Review` |
|
||||
| `stage == "testing"` | `Testing` |
|
||||
| `stage == "deploy"` | `⏸️ Awaiting Deploy — ожидание Confirm Deploy` |
|
||||
| `stage == "done"` | `Done` |
|
||||
| неизвестный/битый `stage` | дефолт: `html`-безопасная строка по `stage` (или `To Analyse`) |
|
||||
|
||||
Этого слоя достаточно для **`⏸️ In Review`** и **`⏸️ Awaiting Deploy`** — оба
|
||||
обязательны по DoD и **работают без сети** (AC-7, AC-8). `In Review` выводится
|
||||
исключительно из brd-clock.
|
||||
|
||||
**Слой 2 — best-effort live-overlay** `_live_plane_branch_override(repo, plane_issue_id,
|
||||
base_label) -> str` для веток, неразличимых offline: **Needs Input, Blocked, Rejected,
|
||||
Cancelled, Deploying, Monitoring after Deploy**. Алгоритм:
|
||||
|
||||
1. Резолв `project_id` по `repo` (`get_project_by_repo(repo).plane_project_id`).
|
||||
2. `live_uuid = fetch_issue_state(plane_issue_id, project_id)` (ORCH-060) — **с коротким
|
||||
таймаутом** (см. Р-4), не дефолтным 10s.
|
||||
3. Сопоставление `live_uuid` с **конкретными** UUID веток из
|
||||
`get_project_states(project_id)` (кэш ORCH-010): `needs_input`, `blocked`,
|
||||
`cancelled`, `rejected`, `deploying`, `monitoring`.
|
||||
4. Override применяется **только** если `live_uuid` совпал с одним из этих ключей.
|
||||
Иначе возвращается `base_label` (offline-метка).
|
||||
|
||||
**Прецеденс (порядок приоритета):**
|
||||
1. Если offline-ядро дало **`⏸️ In Review`** (brd-clock) — overlay **не вызывается**:
|
||||
brd-clock авторитетнее возможно-устаревшего Plane-чтения для In Review.
|
||||
2. Иначе `base_label` = offline-метка, затем применяется overlay (если включён и удался).
|
||||
|
||||
**Анти-false-positive на enduro (важно):** на enduro-trails ключи `deploying`/
|
||||
`monitoring` алиасят UUID `in_progress`/`done` (`_STATE_ALIAS_FALLBACK`), поэтому прямое
|
||||
сравнение UUID дало бы ложный `Deploying` для любой `in_progress`-задачи. Поэтому для
|
||||
`deploying`/`monitoring` override применяется **только если** их UUID в
|
||||
`get_project_states` **отличается** от UUID базового ключа (т.е. проект реально завёл
|
||||
отдельный статус — это ORCH, не enduro). Ключи `needs_input/blocked/cancelled/rejected`
|
||||
имеют отдельные UUID и на enduro, и на ORCH (`_DEFAULT_STATES`), поэтому различимы всегда.
|
||||
|
||||
### Р-2. Fail-safe и невлияние на конвейер (overlay)
|
||||
|
||||
- `_live_plane_branch_override` обёрнут в `try/except` и **никогда не бросает**; любая
|
||||
ошибка/таймаут/нет сети/нет данных → возвращается `base_label`. Это удовлетворяет
|
||||
«без сети не падать» и AC-9 (рендер карточки никогда не падает).
|
||||
- Нет `plane_issue_id` / нет `project_id` / нет креды → overlay не вызывается, метка =
|
||||
offline-ядро.
|
||||
- **Kill-switch:** новый флаг конфигурации `tracker_live_status: bool = True`
|
||||
(env `ORCH_TRACKER_LIVE_STATUS`). При `False` overlay полностью отключён (никаких
|
||||
сетевых чтений в рендере) — карточка деградирует на offline-ядро. Это аварийный
|
||||
тумблер и страховка от регресса для не-ORCH проектов. **Дефолт `True`**, иначе
|
||||
обязательный по DoD `Needs Input` не отобразится из коробки.
|
||||
|
||||
### Р-3. Кэш live-статуса (защита hot-path)
|
||||
|
||||
`render_task_tracker` вызывается на КАЖДОМ обновлении трекера (старт/финиш агента,
|
||||
переход стадии), а в режиме `bump` — с delete+send каждый раз. Чтобы серия быстрых
|
||||
перерисовок не била по Plane:
|
||||
|
||||
- Добавить **TTL-кэш per-issue** для `live_uuid` (ключ — `plane_issue_id`, TTL
|
||||
`tracker_live_status_ttl_s: int = 60`). По образцу `_STATES_CACHE` в `plane_sync.py`.
|
||||
- На промахе кэша — один `fetch_issue_state` с коротким таймаутом; результат кладётся в
|
||||
кэш. На любой ошибке кэш не портится, возвращается offline-метка.
|
||||
|
||||
Это ограничивает сетевую нагрузку overlay ~одним GET в `TTL` на задачу.
|
||||
|
||||
### Р-4. Короткий таймаут live-чтения в рендере
|
||||
|
||||
`fetch_issue_state` (ORCH-060) хардкодит `timeout=10`. Для пути рендера это слишком
|
||||
долго (рендер синхронный, в линии переходов общего конвейера). Решение: добавить в
|
||||
`fetch_issue_state` **необязательный параметр `timeout`** (дефолт прежний `10` —
|
||||
обратная совместимость для reconciler), а overlay вызывает его с
|
||||
`settings.tracker_live_status_timeout_s` (дефолт **3** с). Поведение/сигнатуры
|
||||
существующих вызовов не меняются.
|
||||
|
||||
### Р-5. Единый хелпер кликабельного номера `plane_issue_link`
|
||||
|
||||
Добавить в `src/notifications.py`:
|
||||
|
||||
```python
|
||||
def plane_issue_link(work_item_id, plane_issue_id=None, project_id=None, repo=None) -> str:
|
||||
"""HTML с кликабельным номером (<a href=...>ORCH-NNN</a>) или html.escape(work_item_id).
|
||||
Никогда не падает."""
|
||||
```
|
||||
|
||||
- Переиспользовать логику и guard'ы `_build_plane_issue_link` (ORCH-017), **разнеся**
|
||||
«текст ссылки = номер задачи» и «текст ссылки = `✅ Задача в Plane`», чтобы не
|
||||
дублировать резолв проекта и loopback-guard. Рекомендуется выделить приватный
|
||||
`_plane_issue_url(repo, plane_issue_id, project_id) -> str | None` (сборка URL +
|
||||
loopback/workspace/project guard), который зовут оба: `plane_issue_link` (текст =
|
||||
номер) и `_build_plane_issue_link` (текст = «✅ Задача в Plane»).
|
||||
- База URL: `plane_web_url` → fallback `plane_api_url`; loopback → «нет web URL»
|
||||
(`_is_loopback_base`).
|
||||
- `project_id`: явный аргумент → иначе резолв по `repo`.
|
||||
- URL: `{web_base}/{workspace}/projects/{project_id}/issues/{plane_issue_id}/`.
|
||||
- Текст = `html.escape(work_item_id)`; `href` = `html.escape(url, quote=True)`.
|
||||
- **Fail-safe:** не хватает любого из (web_base/не-loopback, workspace, project_id,
|
||||
plane_issue_id) → вернуть `html.escape(work_item_id)` (номер без ссылки). Никогда не
|
||||
бросает (AC-11, AC-12).
|
||||
|
||||
### Р-6. Доступ к `plane_issue_id`/`project_id` в точках уведомлений (требование 4)
|
||||
|
||||
В большинстве точек `send_telegram` доступен только `work_item_id`. Решение —
|
||||
тонкая fail-safe обёртка по образцу `_get_task_link_fields`:
|
||||
|
||||
```python
|
||||
def link_for(work_item_id, task_id=None) -> str:
|
||||
"""По work_item_id (или task_id) достать repo+plane_issue_id из БД и вернуть
|
||||
plane_issue_link(...). На любой нехватке данных -> html.escape(work_item_id)."""
|
||||
```
|
||||
|
||||
- Если у точки есть `task_id` — читать `(repo, plane_issue_id)` напрямую из `tasks` по
|
||||
`id`. Если только `work_item_id` — `SELECT repo, plane_issue_id FROM tasks WHERE
|
||||
work_item_id=? ORDER BY id DESC LIMIT 1` (как в `_resolve_project_id`).
|
||||
- Везде, где данных нет — деградация на `html.escape(work_item_id)`, без падения.
|
||||
- Применить во всех точках из TZ §3.3 (`notify_approve_requested`, `notify_error`,
|
||||
`stage_engine`, `launcher`, `merge_gate`, `job_reaper`, `security_gate`, `reconciler`,
|
||||
`main`) — **только там, где упоминается номер задачи**.
|
||||
|
||||
### Р-7. `tracker_mode` дефолт → `bump`
|
||||
|
||||
`src/config.py`: `tracker_mode: str = "edit"` → `"bump"`. Инвариант «одна карточка на
|
||||
задачу» сохранён в обоих режимах (код `update_task_tracker` не меняется по сути).
|
||||
`edit` остаётся доступен через `ORCH_TRACKER_MODE=edit`. Транзиентный фейл `send` не
|
||||
обнуляет `tracker_message_id` (инвариант уже в коде — сохранить).
|
||||
|
||||
### Р-8. Чего НЕ делаем (границы)
|
||||
|
||||
- НЕ менять схему БД, `STAGE_TRANSITIONS`, Quality Gates, транспорт
|
||||
`send_telegram`/`edit_telegram`/`delete_telegram`, `disable_notification`-семантику.
|
||||
- НЕ менять поведение агентов/конвейера. Слой B (индикация) не управляет слоем A.
|
||||
- НЕ добавлять блокирующих сетевых ожиданий в линию переходов сверх одного короткого
|
||||
best-effort GET с кэшем (Р-3/Р-4).
|
||||
- НЕ создавать глобальный (сквозной) ADR: изменение локально для `notifications.py` +
|
||||
один config-дефолт, не вводит новую стадию/QG/компонент. Достаточно per-work-item ADR.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы**
|
||||
- Обязательные по DoD `⏸️ In Review`, `⏸️ Awaiting Deploy` работают **без сети**
|
||||
(детерминированно, тестируемо offline — AC-6/AC-7).
|
||||
- `❓ Needs Input` (и Blocked/Rejected/Cancelled/Deploying/Monitoring) отражаются через
|
||||
авторитетный источник — живой Plane-статус, который иначе невосстановим из БД.
|
||||
- Единый хелпер ссылки убирает дублирование резолва проекта/loopback-guard (ORCH-017).
|
||||
- Kill-switch + кэш + короткий таймаут ограничивают риск для общего инстанса.
|
||||
|
||||
**Минусы / ограничения**
|
||||
- Overlay добавляет ≤1 короткий GET (3 с таймаут) на задачу в `TTL=60s` в путь рендера.
|
||||
Митигировано кэшем, таймаутом и kill-switch.
|
||||
- При недоступном Plane ветки `Needs Input`/`Blocked`/… деградируют на offline-метку
|
||||
(`Analysis`/stage). Это осознанный, безопасный компромисс (рендер важнее точности
|
||||
ветки; конвейер не блокируется).
|
||||
- На частично сконфигурированном проекте без отдельных статусов `Deploying`/`Monitoring`
|
||||
эти ветки не показываются (alias-guard) — корректная деградация, не баг.
|
||||
|
||||
**Риски** — см. `10-tech-risks.md`.
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
- **Только offline (вариант б TZ).** Отклонён: не отличает `Needs Input` от `Analysis`
|
||||
→ не покрывает обязательный AC-8.
|
||||
- **Чтение `01-questions.md` из worktree как offline-сигнал Needs Input.** Отклонён:
|
||||
хрупко (резолв пути worktree из `notifications.py`, файл может пережить ответ,
|
||||
гонки) — менее надёжно, чем авторитетный Plane-статус.
|
||||
- **Добавить DB-колонку-флаг для ветки.** Запрещено TZ (без изменения схемы).
|
||||
- **Асинхронный фон/демон для подтяжки статуса.** Избыточно для слоя индикации; кэш +
|
||||
короткий таймаут дешевле и проще, без нового компонента.
|
||||
46
docs/work-items/ORCH-067/07-infra-requirements.md
Normal file
46
docs/work-items/ORCH-067/07-infra-requirements.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Инфраструктурные требования — ORCH-067
|
||||
|
||||
Топология не меняется (никаких новых контейнеров/портов/сервисов). Изменения —
|
||||
**только конфигурация/env** и обязательный staging-гейт (self-hosting).
|
||||
|
||||
## 1. Изменения конфигурации (`src/config.py`)
|
||||
|
||||
| Поле | env | Старое | Новое | Назначение |
|
||||
|---|---|---|---|---|
|
||||
| `tracker_mode` | `ORCH_TRACKER_MODE` | `"edit"` | `"bump"` (дефолт) | Карточка падает вниз ленты при обновлении (ADR-001 Р-7). `edit` доступен через env. |
|
||||
| `tracker_live_status` | `ORCH_TRACKER_LIVE_STATUS` | — (нет) | `True` (дефолт) | Kill-switch live-overlay Plane-статуса (ADR-001 Р-2). `0/false` → только offline-метки, без сетевых чтений в рендере. |
|
||||
| `tracker_live_status_ttl_s` | `ORCH_TRACKER_LIVE_STATUS_TTL_S` | — | `60` | TTL per-issue кэша live-статуса (ADR-001 Р-3). |
|
||||
| `tracker_live_status_timeout_s` | `ORCH_TRACKER_LIVE_STATUS_TIMEOUT_S` | — | `3` | Короткий таймаут live-чтения в рендере (ADR-001 Р-4). |
|
||||
|
||||
Уже существующие (не менять, использовать): `plane_web_url`
|
||||
(`ORCH_PLANE_WEB_URL`, прод — `https://plane.mva154.duckdns.org`),
|
||||
`plane_workspace_slug` (прод — `ag_proj`), `plane_api_url`.
|
||||
|
||||
## 2. `.env` / `.env.example`
|
||||
|
||||
- Обновить `.env.example`: добавить `ORCH_TRACKER_MODE`, `ORCH_PLANE_WEB_URL`,
|
||||
`ORCH_TRACKER_LIVE_STATUS*` с дефолтами и комментариями (канон настроек —
|
||||
`.env.example`, реальные секреты не коммитить).
|
||||
- На прод-хосте допустимо явно выставить `ORCH_TRACKER_MODE=bump` как страховку, но код
|
||||
обязан работать «из коробки» и без env.
|
||||
- `ORCH_PLANE_WEB_URL` должен быть задан на проде (иначе номер задачи деградирует на
|
||||
текст без ссылки — fail-safe, не падение).
|
||||
|
||||
## 3. Self-hosting (обязательно)
|
||||
|
||||
- **НЕ перезапускать / не ронять** прод-контейнер `orchestrator` (8500) в рамках задачи —
|
||||
общий инстанс/БД с enduro-trails.
|
||||
- Обязательная страховка через `deploy-staging` (8501, изолированная БД) **до** прод-деплоя.
|
||||
На staging проверить:
|
||||
- режим `bump`: одна карточка на задачу, падает вниз, тихо (без звука), без дублей;
|
||||
- статус-строка: `⏸️ In Review`, `⏸️ Awaiting Deploy`, `❓ Needs Input` отображаются;
|
||||
- кликабельный номер ведёт на страницу Plane;
|
||||
- **нет регресса для enduro-trails** (карточка без новых статусов деградирует корректно).
|
||||
- Прод-деплой орка — только переводом задачи на стадии `deploy` в статус
|
||||
**«Confirm Deploy»** (ORCH-059), не `Approved`.
|
||||
|
||||
## 4. Сетевые требования
|
||||
|
||||
- Live-overlay требует доступности Plane API (`plane_api_url`) из контейнера — он уже
|
||||
есть (используется plane_sync). Недоступность Plane → graceful degrade на offline-метку,
|
||||
конвейер не блокируется (короткий таймаут + kill-switch).
|
||||
35
docs/work-items/ORCH-067/08-data-requirements.md
Normal file
35
docs/work-items/ORCH-067/08-data-requirements.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Требования к данным — ORCH-067
|
||||
|
||||
## Изменения схемы БД: НЕТ
|
||||
|
||||
`STAGE_TRANSITIONS`, таблицы и колонки `tasks`/`agent_runs` **не меняются**. Это жёсткое
|
||||
ограничение TZ §6 и предпосылка ADR-001 (запрет колонки-флага для веток статуса).
|
||||
|
||||
## Читаемые колонки `tasks` (существующие)
|
||||
|
||||
| Колонка | Использование в ORCH-067 |
|
||||
|---|---|
|
||||
| `id` | Ключ задачи. |
|
||||
| `work_item_id` | Текст номера (`ORCH-NNN`) + ключ резолва в `link_for`. |
|
||||
| `title` | Заголовок карточки (`html.escape`). |
|
||||
| `stage` | Offline-маппинг Plane-статуса (ADR-001 Р-1, слой 1). |
|
||||
| `brd_review_started_at`, `brd_review_ended_at` | Различение `Analysis` ↔ `⏸️ In Review` (offline, без сети). |
|
||||
| `repo` | Резолв `project_id` (`get_project_by_repo`) для ссылки и live-overlay. |
|
||||
| `plane_issue_id` (UUID) | `issue_id` в URL Plane + аргумент `fetch_issue_state` (live-overlay). |
|
||||
| `created_at`, `updated_at` | Тоталы времени в done-строке (без изменений). |
|
||||
|
||||
`render_task_tracker` **расширяет существующий `SELECT`** по `tasks`, добавляя `repo` и
|
||||
`plane_issue_id` к уже выбираемым полям. Схему это не трогает — колонки уже есть.
|
||||
|
||||
## Кэш в памяти (не БД)
|
||||
|
||||
Per-issue TTL-кэш live-статуса (ключ `plane_issue_id`, TTL
|
||||
`tracker_live_status_ttl_s=60`, ADR-001 Р-3) — **in-memory**, по образцу `_STATES_CACHE`
|
||||
в `plane_sync.py`. Не персистится, переживание рестарта не требуется (best-effort
|
||||
индикация). Очистка при рестарте — допустима.
|
||||
|
||||
## Источник имён статусов
|
||||
|
||||
Имена и логические ключи статусов берутся из существующих структур `src/plane_sync.py`
|
||||
(`_PLANE_NAME_TO_KEY`, `get_project_states`, `_DEFAULT_STATES`), вводимых ORCH-066.
|
||||
Новых статусов/ключей ORCH-067 **не добавляет**.
|
||||
21
docs/work-items/ORCH-067/10-tech-risks.md
Normal file
21
docs/work-items/ORCH-067/10-tech-risks.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Технические риски — ORCH-067
|
||||
|
||||
| # | Риск | Вероятность / Влияние | Митигация (ADR-001) | Остаточный риск |
|
||||
|---|---|---|---|---|
|
||||
| R-1 | **Регресс enduro-trails** при смене дефолта `tracker_mode` → `bump` (другое поведение карточки для всех проектов). | Сред / Сред | Инвариант «одна карточка на задачу» сохранён; `edit` доступен через env; проверка на staging + тесты нерегресса (AC-16). | Низкий |
|
||||
| R-2 | **Поломка HTML-разметки** неэкранированным `title`/причиной → сообщение с `parse_mode=HTML` не доставится. | Сред / Сред | Обязательный `html.escape` для всего пользовательского текста; `href` через `html.escape(url, quote=True)`; тест с `<b>`/`&` (AC-14). | Низкий |
|
||||
| R-3 | **Latency в hot-path конвейера**: live-overlay добавляет сетевой GET в синхронный рендер, вызываемый на каждом переходе/в bump. | Сред / Сред | Короткий таймаут 3 с (Р-4) + per-issue TTL-кэш 60 с (Р-3) + kill-switch `ORCH_TRACKER_LIVE_STATUS=0` (Р-2). ≤1 GET на задачу за TTL. | Низкий |
|
||||
| R-4 | **Рендер карточки падает** на битых данных/недоступном Plane. | Низк / Выс | `plane_status_label` чистая и never-raise; overlay в `try/except` → degrade на offline-метку; `render_task_tracker` уже never-raise (AC-9). | Очень низкий |
|
||||
| R-5 | **Ложный `Deploying`/`Monitoring` на enduro** (их UUID алиасит `in_progress`/`done`). | Сред / Низк | Override этих веток только если UUID статуса ≠ UUID базового ключа в `get_project_states` (Р-1, anti-false-positive). | Очень низкий |
|
||||
| R-6 | **Устаревший Plane-статус из кэша** показывает неактуальную ветку (например, `Needs Input` после ответа). | Сред / Низк | TTL 60 с самозаживает; offline-ядро авторитетно для In Review (brd-clock не оверрайдится). Индикация, не управление — расхождение косметическое. | Низкий |
|
||||
| R-7 | **Транзиентный фейл `send` плодит дубли / обнуляет указатель** в bump. | Низк / Сред | Инвариант уже в коде (`set_tracker_message_id` только при `new_mid is not None`); не менять; тест AC-3. | Низкий |
|
||||
| R-8 | **Self-hosting**: деплой орка ломает общий инстанс (enduro + ORCH, общая БД/очередь). | Низк / Выс | Обязательный staging-гейт (8501) до прода; прод-контейнер не ронять в задаче; прод-деплой только через «Confirm Deploy». | Низкий |
|
||||
| R-9 | **Пропущенная точка** уведомления с сырым номером (требование 4 — много call-sites). | Сред / Низк | Единый `link_for`/`plane_issue_link`; чек-лист точек из TZ §3.3; reviewer проверяет покрытие (AC-13). | Низкий |
|
||||
| R-10 | **Рассинхрон имён статусов** с ORCH-066, если та не в проде на момент разработки. | Низк / Низк | Имена берутся из `_PLANE_NAME_TO_KEY` (golden source); делать после прода ORCH-066 (BRD §6). | Низкий |
|
||||
|
||||
## Сводно
|
||||
|
||||
Все остаточные риски — низкие/очень низкие после митигаций. Главные защитные контуры:
|
||||
(1) offline-ядро статуса не требует сети и детерминировано; (2) live-overlay полностью
|
||||
best-effort с таймаутом+кэшем+kill-switch; (3) обязательный staging-гейт перед прод-деплоем
|
||||
общего инстанса (self-hosting).
|
||||
78
docs/work-items/ORCH-067/12-review.md
Normal file
78
docs/work-items/ORCH-067/12-review.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-067
|
||||
verdict: APPROVED
|
||||
version: 2
|
||||
---
|
||||
|
||||
# Review ORCH-067
|
||||
|
||||
## Summary
|
||||
|
||||
Повторное ревью после фикса документации (коммит `7a88f39`). Реализация полностью
|
||||
соответствует ТЗ (`02-trz.md`), ADR-001 и всем acceptance criteria (`03-acceptance-criteria.md`).
|
||||
|
||||
**Код** (`src/notifications.py` — ядро):
|
||||
- **Req 1 (bump):** дефолт `tracker_mode` сменён `edit → bump` (`src/config.py`); логика
|
||||
`update_task_tracker`, транспорт `send/edit/delete_telegram`, `disable_notification` и
|
||||
инвариант «одна карточка на задачу» не тронуты (AC-1..AC-4, AC-15 ✓).
|
||||
- **Req 2 (статус-строка):** чистый never-raise `plane_status_label(task_row)` (offline-ядро:
|
||||
stage→статус + `⏸️ In Review` из brd-clock + `⏸️ Awaiting Deploy`, всё без сети) +
|
||||
best-effort `_live_plane_branch_override` для ветвей, неотличимых offline (Needs Input /
|
||||
Blocked / Rejected / Cancelled / Deploying / Monitoring). Kill-switch
|
||||
(`tracker_live_status`), per-issue TTL-кэш (`_LIVE_STATE_CACHE`), короткий таймаут
|
||||
(`fetch_issue_state(..., timeout=)`, дефолт 10 сохранён → нет регресса reconciler).
|
||||
Anti-false-positive guard для enduro (`_LIVE_BRANCH_BASE`: deploying/monitoring override
|
||||
только при отдельном UUID). Прецеденс In Review > overlay соблюдён. `_card_status_label`
|
||||
обёрнут в try/except → рендер никогда не падает (AC-5..AC-9 ✓).
|
||||
- **Req 3+4 (кликабельный номер):** единый `_plane_issue_url` устраняет дублирование
|
||||
резолва проекта/loopback-guard (ORCH-017); `plane_issue_link` (текст=номер) и
|
||||
`_build_plane_issue_link` (текст=«✅ Задача в Plane») оба зовут его. `link_for` fail-safe
|
||||
достаёт `repo`/`plane_issue_id` из БД. Применено в заголовке карточки и во ВСЕХ точках
|
||||
§3.3 с номером задачи (AC-10..AC-14 ✓).
|
||||
|
||||
**Точки §3.3 проверены пофайлово:** `notify_approve_requested`, `notify_error`,
|
||||
`stage_engine.py` (все alert'ы с номером), `agents/launcher.py`, `security_gate.py`,
|
||||
`reconciler.py` — номер кликабелен. `merge_gate.py`/`job_reaper.py`/`main.py` оставлены без
|
||||
ссылки **осознанно и корректно**: их тексты ссылаются на repo/job/run_id, а НЕ на
|
||||
`work_item_id` (проверено: merge_gate:432 — lease/repo, job_reaper:396 — job/agent/repo,
|
||||
main:47 — orphaned run_ids).
|
||||
|
||||
**Инварианты/нерегресс:** схема БД, `STAGE_TRANSITIONS`, QG, транспорт — не тронуты
|
||||
(AC-15 ✓). `get_db()` возвращает новое соединение на вызов, поэтому `conn.close()` в
|
||||
`link_for` корректен. `pytest tests/ -q` → **907 passed** (AC-16, AC-17 ✓).
|
||||
|
||||
**Документация (блокеры v1 закрыты):** `CHANGELOG.md`, `CLAUDE.md`, `.env.example`
|
||||
обновлены в коммите `7a88f39`; ADR-001 присутствует и полон; `README.md`/`internals.md`
|
||||
синхронизированы (AC-18 ✓).
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- (нет)
|
||||
|
||||
### P1 — Must fix
|
||||
- (нет)
|
||||
|
||||
### P2 — Should fix
|
||||
- (нет)
|
||||
|
||||
### P3 — Nice to have (не блокирует)
|
||||
- [ ] Часть alert-сообщений в `stage_engine.py` (`_handle_self_deploy_phase_b`,
|
||||
`_handle_merge_verify`) встраивает «сырой» `{msg}`/`{e}`/`{reason}` рядом с новой
|
||||
`<a>`-ссылкой; под `parse_mode=HTML` редкий `<` в этих подстановках теоретически мог
|
||||
бы помешать рендеру. Это **пре-существующее поведение** (parse_mode=HTML стоял и
|
||||
раньше), не регресс данной задачи; `notify_error` свой `error` экранирует. Можно при
|
||||
случае обернуть прочие подстановки в `html.escape`.
|
||||
|
||||
## Документация
|
||||
|
||||
- `docs/architecture/README.md` — обновлён (компонент Notifications / live-tracker). ✓
|
||||
- `docs/architecture/internals.md` — обновлён (§7: bump/edit, Plane-статус, кликабельный номер). ✓
|
||||
- `06-adr/ADR-001-tracker-plane-status-and-link.md` — присутствует, полный, закрывает все `[ARCH]`. ✓
|
||||
- `CHANGELOG.md` — обновлён (запись ORCH-067). ✓
|
||||
- `CLAUDE.md` — обновлён (раздел «Нотификации / Telegram live-tracker»). ✓
|
||||
- `.env.example` — синхронизирован (`ORCH_TRACKER_MODE=bump` + новые флаги live-overlay). ✓
|
||||
|
||||
Документация = golden source: код и доку обновлены в одном PR. Блокеры предыдущего ревью
|
||||
(v1) закрыты. Замечаний уровня P0/P1/P2 нет → **APPROVED**.
|
||||
78
docs/work-items/ORCH-067/13-test-report.md
Normal file
78
docs/work-items/ORCH-067/13-test-report.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-067
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-067
|
||||
|
||||
Telegram tracker: bump по умолчанию, статус-строка карточки по модели Plane (ORCH-066),
|
||||
кликабельный номер задачи в карточке и во всех уведомлениях орка.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Ветка: `feature/ORCH-067-telegram-tracker-bump-plane` (worktree)
|
||||
- Дата: 2026-06-08
|
||||
- Review-вердикт: APPROVED (`12-review.md`, version 2)
|
||||
|
||||
## Smoke test API (prod, :8500)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | PASS — отдаёт active_tasks (ORCH-067 на stage=testing) |
|
||||
| `GET /queue` | PASS — breaker closed, preflight_ok, counts корректны |
|
||||
|
||||
Прод-контейнер не перезапускался (self-hosting инвариант соблюдён).
|
||||
|
||||
## Результаты по тест-плану (04-test-plan.yaml)
|
||||
|
||||
| TC ID | Описание | Модуль | AC | Результат |
|
||||
|-------|----------|--------|----|-----------|
|
||||
| TC-01 | Дефолт `tracker_mode == "bump"` без env | test_tracker_bump_default.py | AC-1 | PASS |
|
||||
| TC-02 | bump: delete(old)→send(silent)→repoint, одна карточка | test_tracker_bump_default.py | AC-2 | PASS |
|
||||
| TC-03 | bump fail-safe: send=None не обнуляет указатель | test_tracker_bump_default.py | AC-3 | PASS |
|
||||
| TC-04 | `ORCH_TRACKER_MODE=edit` — прежнее поведение | test_tracker_bump_default.py | AC-4 | PASS |
|
||||
| TC-05 | Карточка содержит строку Plane-статуса | test_tracker_status_line.py | AC-5 | PASS |
|
||||
| TC-06 | Маппинг stage → Plane-статус (§2.2, параметризованный) | test_tracker_status_line.py | AC-6 | PASS |
|
||||
| TC-07 | In Review из brd-clock, без сети; строка «Подтверждение BRD» сохранена | test_tracker_status_line.py | AC-7 | PASS |
|
||||
| TC-08 | Awaiting Deploy + Needs Input отражены | test_tracker_status_line.py | AC-8 | PASS |
|
||||
| TC-09 | render_task_tracker не падает на битых данных | test_tracker_status_line.py | AC-9, AC-16 | PASS |
|
||||
| TC-10 | Кликабельный номер в карточке при полных данных | test_tracker_issue_link.py | AC-10 | PASS |
|
||||
| TC-11 | Fail-safe ссылки в карточке (параметризованный) | test_tracker_issue_link.py | AC-11 | PASS |
|
||||
| TC-12 | `plane_issue_link(...)` — ссылка/escape, никогда не бросает | test_plane_issue_link.py | AC-12 | PASS |
|
||||
| TC-13 | notify_approve_requested: номер кликабелен, одна нотификация | test_notify_issue_links.py | AC-13 | PASS |
|
||||
| TC-14 | notify_error: кликабелен/деградирует без падения | test_notify_issue_links.py | AC-13, AC-12 | PASS |
|
||||
| TC-15 | Точки send_telegram (stage_engine/launcher/merge_gate/job_reaper/security_gate/reconciler/main) используют хелпер | test_notify_issue_links.py | AC-13 | PASS |
|
||||
| TC-16 | HTML-экранирование title/`&`, валидность `<a>` | test_tracker_issue_link.py | AC-14 | PASS |
|
||||
| TC-17 | Инварианты транспорта: disable_notification, одна карточка | test_tracker_bump_default.py | AC-15 | PASS |
|
||||
| TC-18 | Нерегресс нотификаций + деградация для enduro-trails | test_notify_done_regression.py | AC-16, AC-17 | PASS |
|
||||
|
||||
Все 18 TC из тест-плана — PASS. Целевые модули: **57 passed**.
|
||||
|
||||
## Покрытие acceptance criteria
|
||||
AC-1..AC-18 — все покрыты соответствующими TC и зелёные. AC-17 (полный набор) подтверждён
|
||||
прогоном всего пакета.
|
||||
|
||||
## Вывод pytest (полный регресс)
|
||||
|
||||
```
|
||||
$ python -m pytest tests/ -v --tb=short
|
||||
...
|
||||
======================= 907 passed, 1 warning in 22.36s ========================
|
||||
```
|
||||
|
||||
Единственный warning — пре-существующий `PydanticDeprecatedSince20` в `src/config.py:4`
|
||||
(не относится к ORCH-067, не регресс).
|
||||
|
||||
Целевые модули задачи:
|
||||
```
|
||||
$ python -m pytest tests/test_tracker_bump_default.py tests/test_tracker_status_line.py \
|
||||
tests/test_tracker_issue_link.py tests/test_plane_issue_link.py \
|
||||
tests/test_notify_issue_links.py tests/test_notify_done_regression.py -q
|
||||
57 passed, 1 warning in 1.39s
|
||||
```
|
||||
|
||||
## Итог
|
||||
**PASS** — 907/907 тестов зелёные, все 18 TC и AC-1..AC-18 выполнены, smoke API OK,
|
||||
нерегресс для enduro-trails подтверждён. Задача готова к переходу на `deploy-staging`.
|
||||
12
docs/work-items/ORCH-067/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-067/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-067
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
32
docs/work-items/ORCH-067/15-staging-log.md
Normal file
32
docs/work-items/ORCH-067/15-staging-log.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
timestamp: 2026-06-08T10:32:02Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed. All REAL checks passed (8/10), the two
|
||||
sandbox-infra checks were waived per ORCH-061 → exit 0 → SUCCESS.
|
||||
|
||||
Canonical run (ORCH-048, ADR-001) inside the staging container:
|
||||
|
||||
```
|
||||
docker exec orchestrator-staging \
|
||||
python3 /repos/orchestrator/scripts/staging_check.py \
|
||||
--base-url http://localhost:8501 --mode stub
|
||||
```
|
||||
|
||||
## Result: 8/10 checks PASS — VERDICT SUCCESS (exit 0)
|
||||
|
||||
- Block A (SMOKE): A1 /health, A2 /queue, A3 ORCH_STAGING=true — PASS
|
||||
- Block B (ACCESS): B4 Plane sandbox, B5 Gitea push, B6 registry isolation — PASS
|
||||
- Block C (E2E, stub): C7 create issue, C8 trigger pipeline — PASS
|
||||
|
||||
REAL failed: none
|
||||
|
||||
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
|
||||
|
||||
Cleanup completed: test Plane SANDBOX issue deleted (HTTP 204), no branch created to delete.
|
||||
14
docs/work-items/ORCH-067/16-post-deploy-log.md
Normal file
14
docs/work-items/ORCH-067/16-post-deploy-log.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY
|
||||
action_taken: NONE
|
||||
work_item: ORCH-067
|
||||
window_s: 900
|
||||
checks_total: 30
|
||||
checks_failed: 0
|
||||
---
|
||||
|
||||
# Post-deploy log — ORCH-021 post-deploy monitor
|
||||
|
||||
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
|
||||
|
||||
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.
|
||||
7
docs/work-items/ORCH-068/00-business-request.md
Normal file
7
docs/work-items/ORCH-068/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: BUG: reconciler livelock — спам unblock done-задачи (ET-002)
|
||||
|
||||
Work Item ID: ORCH-068
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
52
docs/work-items/ORCH-068/01-brd.md
Normal file
52
docs/work-items/ORCH-068/01-brd.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# BRD — ORCH-068: BUG reconciler livelock (спам unblock done-задачи)
|
||||
|
||||
## 1. Контекст и предыстория
|
||||
Reconciler (`src/reconciler.py`, ORCH-053) — фоновый поток, который доигрывает пропущенные webhook-переходы. Ветвь **F-2 (plane-side)** опрашивает Plane per-project и реплеит In Progress / Approved / Rejected через штатные `handle_status_start` / `handle_verdict`. При фактической разблокировке вызывается `_note_unblock` → лог + Telegram.
|
||||
|
||||
ORCH-066 (мердж `feature/ORCH-066-plane`, прод 2026-06-07 ~22:16 UTC) ввела новую статусную модель Plane (маппинг стадия↔статус, новые имена `Done`, `Monitoring after Deploy` и т.п.). Это спровоцировало регрессию в F-2.
|
||||
|
||||
## 2. Проблема (бизнес-симптом)
|
||||
С 22:17 UTC (рестарт прод-контейнера после деплоя ORCH-066) reconciler каждые ~120с шлёт в Telegram:
|
||||
```
|
||||
reconciler: ET-002 done разблокирована (потерян webhook)
|
||||
```
|
||||
для задачи **ET-002** (enduro-trails), которая в Done с 2026-05-21. На момент анализа — 191+ сообщений подряд, поток не прекращается (ночной спам, найден Славой 2026-06-08 01:22 UTC).
|
||||
|
||||
**Ключевой факт:** ET-002 полностью синхронизирована — БД `stage=done`, Plane `state=Done`. Reconciler обязан молчать (инвариант «silence when in sync», AC-9/AC-10 из ORCH-053), но шлёт уведомления вхолостую.
|
||||
|
||||
## 3. Диагностика (проведена, root cause найден)
|
||||
1. **Деньги/токены НЕ тратятся:** `jobs` / `agent_runs` за 4ч пусты; `advance_stage` для done = no-op; `handle_verdict` для done-задачи ничего не меняет. Это «дешёвый», но шумный и подрывающий доверие баг (livelock + ложный alert-fatigue).
|
||||
2. **Механизм:**
|
||||
- `_reconcile_plane_project` (`src/reconciler.py` ~241) тянет `list_issues_by_state(pid, [in_progress, approved, rejected])`.
|
||||
- На enduro-trails статусы «схлопнуты»: после ORCH-066 терминальный `Done` алиасится под UUID `approved` (см. ниже п.4) → ET-002 (Plane=Done) **попадает** в actionable-выборку.
|
||||
- В `_reconcile_plane_issue` (~295) срабатывает ветка `new_state == approved and task is not None` → `handle_verdict(approved)` (no-op, задача уже done) **+ безусловный `_note_unblock`**.
|
||||
- `_note_unblock` (~317) вызывается **сразу после `_dispatch`, не проверяя фактическое изменение состояния** — хотя его docstring обещает «fires only on an actual state change, never per idle tick». Инвариант нарушен.
|
||||
3. **Два независимых дефекта складываются:**
|
||||
- **D1 (выборка):** терминальные статусы (`Done`/`Cancelled`) не исключены из actionable-набора F-2; на «схлопывающих» проектах Done не отличается от approved по голому UUID.
|
||||
- **D2 (нотификация):** `_note_unblock` срабатывает безусловно после no-op dispatch, а не только при подтверждённом state change.
|
||||
4. **Почему `get_project_states` схлопывает:** функция строит маппинг по *именам* статусов из Plane API, затем недостающие ключи добивает из `_DEFAULT_STATES` (enduro-значения). После ORCH-066 набор статусов enduro изменился — голый UUID перестал однозначно различать `Done` (completed-группа) и `approved` (review). Группа состояния (`state.group`) при этом различает их корректно, но в коде не используется.
|
||||
|
||||
## 4. Связанный баг (BUG КЭША СТАТУСОВ, найден 2026-06-07 при деплое ORCH-066)
|
||||
`_STATES_CACHE` (`src/plane_sync.py` ~134) кэширует статусы Plane на **весь lifetime процесса**. После создания нового Plane-статуса (напр. `Confirm Deploy`) боевой процесс держит устаревший набор → webhook на новый статус даёт «no pipeline action» (Phase B не триггерится). Лечилось только рестартом орка. Примитив сброса уже есть — `reload_project_states()` — но он нигде не вызывается автоматически.
|
||||
|
||||
Оба бага — следствие хрупкости статусной модели после ORCH-066. **Решение:** вести их в одном work item (см. scope ниже), окончательное разделение — на усмотрение архитектора.
|
||||
|
||||
## 5. Цели (Goals)
|
||||
- G1. Reconciler НЕ шлёт «разблокирована» для синхронизированной done/cancelled задачи (восстановить инвариант silence-when-in-sync).
|
||||
- G2. `_note_unblock` срабатывает **только** при реальном state change (соблюдён AC-9/AC-10).
|
||||
- G3. Дедуп: нет повторного спама по той же задаче без изменения её состояния.
|
||||
- G4. Корректное различение терминальных (`Done`/`Cancelled`) и review-статусов (`approved`/`rejected`) даже на проектах, «схлопывающих» их по UUID — на всех проектах (enduro И orchestrator).
|
||||
- G5 (secondary). Устаревший `_STATES_CACHE` обновляется без рестарта процесса (TTL / flush-on-unknown / endpoint).
|
||||
|
||||
## 6. Не-цели (Out of scope)
|
||||
- N1. Менять source-of-truth: ориентир F-2 на Plane **остаётся** корректным по дизайну (таблица `tasks` без status-колонки; статусы двигает человек в Plane). Идею F-2 НЕ переписываем — баг в маппинге/нотификации, не в концепции.
|
||||
- N2. Менять реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, схему БД, контракты гейтов.
|
||||
- N3. Менять поведение F-1 (gate-side) и F-3.
|
||||
- N4. Полный авто-approve деплоя (ORCH-54).
|
||||
|
||||
## 7. Затронутые стороны
|
||||
- **Все проекты на одном инстансе** (enduro-trails + orchestrator, общая БД/очередь) — баг проявился на ET-002, но фикс выборки терминалов обязан быть проектно-независимым.
|
||||
- **Self-hosting:** правка идёт в работающий прод-инструмент → обязательна страховка staging (8501), запрет на рестарт прод-контейнера в рамках задачи.
|
||||
|
||||
## 8. Критерий успеха (бизнес)
|
||||
Тик reconciler для синхронизированной done/cancelled задачи = **0 уведомлений, 0 jobs, 0 токенов**. Telegram-спам прекращён. Легитимная разблокировка (реально потерянный approved/in_progress webhook) по-прежнему работает (нет регресса F-2).
|
||||
68
docs/work-items/ORCH-068/02-trz.md
Normal file
68
docs/work-items/ORCH-068/02-trz.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ТЗ — ORCH-068: устранить livelock reconciler F-2 + спам unblock done-задачи
|
||||
|
||||
> Документ описывает ТРЕБОВАНИЯ к изменению поведения (что и где), а не выбор реализации. Конкретный механизм (state.group vs явный allowlist терминалов; TTL vs flush-on-unknown) — решение архитектора в ADR.
|
||||
|
||||
## 1. Затронутые модули `src/`
|
||||
| Модуль | Роль в баге | Требуемое изменение |
|
||||
|--------|-------------|---------------------|
|
||||
| `src/reconciler.py` | F-2 `_reconcile_plane_project` / `_reconcile_plane_issue` / `_note_unblock` | Исключить терминальные статусы из actionable-выборки; `_note_unblock` только при подтверждённом state change; дедуп. |
|
||||
| `src/plane_sync.py` | `get_project_states`, `list_issues_by_state`, `_STATES_CACHE` | Дать способ различать терминальные/review статусы (группа состояния); устранить вечно-устаревший кэш (TTL/flush). |
|
||||
| `src/config.py` | флаги | (если нужны) новые kill-switch/настройки TTL — с дефолтами, не меняющими прод-инварианты. |
|
||||
| `src/main.py` (`/queue`) | наблюдаемость | (опц.) отразить дедуп/skip-терминалов в снимке `reconcile`. |
|
||||
|
||||
**НЕ трогать:** `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`), схему БД, контракты `handle_status_start` / `handle_verdict`, F-1 (`reconcile_gate_once`), F-3.
|
||||
|
||||
## 2. Требования к F-2 (src/reconciler.py)
|
||||
|
||||
### TR-1. Исключить терминальные статусы из actionable-выборки
|
||||
`_reconcile_plane_project` НЕ должен подавать задачи в терминальном Plane-статусе (`Done` и прочие completed-группы, `Cancelled`) ни в `list_issues_by_state`, ни в последующее сравнение веток.
|
||||
- Требование проектно-независимое: работает на enduro И orchestrator, независимо от того, «схлопывает» ли проект статусы по UUID.
|
||||
- Различение `Done`/`Cancelled` (completed) от `approved`/`rejected` (review) НЕ должно опираться только на голый UUID, если проект их алиасит. Допустимый ориентир — группа состояния Plane (`state.group`: `completed`/`started`/`unstarted`/`backlog`/`cancelled`) либо явный набор логических ключей терминалов. Выбор — за архитектором.
|
||||
|
||||
### TR-2. `_note_unblock` — только при реальном state change
|
||||
`_note_unblock` (лог + Telegram + инкремент `unblocked_total`) ВЫЗЫВАЕТСЯ ТОЛЬКО когда диспетчеризованный обработчик фактически изменил состояние задачи (advance / replayed transition, реально сдвинувший стадию). No-op dispatch (задача уже в целевом состоянии) → нотификация НЕ отправляется.
|
||||
- Сейчас `_dispatch` (`asyncio.run(coro_fn(...))`) отбрасывает результат, а `_note_unblock` зовётся безусловно. Требуется механизм подтверждения изменения (напр. сравнение стадии задачи до/после dispatch через `get_task_by_plane_id`, либо проброс сигнала из обработчика). Конкретику выбирает архитектор; контракт обработчиков `handle_*` менять НЕ обязательно (предпочтительно сравнение состояния до/после на стороне reconciler).
|
||||
- Восстановить соответствие docstring `_note_unblock`: «Fires only on an actual state change … never per idle tick».
|
||||
|
||||
### TR-3. Дедуп / идемпотентность нотификаций
|
||||
Reconciler НЕ должен слать повторное уведомление о той же задаче, если её состояние не менялось с прошлого тика. TR-1+TR-2 закрывают основной кейс (done более не входит в выборку и не нотифицируется); TR-3 — дополнительная страховка (best-effort), чтобы любой будущий no-op путь не дал повторного спама.
|
||||
|
||||
## 3. Требования к статус-кэшу (src/plane_sync.py) — secondary
|
||||
|
||||
### TR-4. Устаревший `_STATES_CACHE` обновляется без рестарта
|
||||
После появления нового Plane-статуса процесс не должен бесконечно держать устаревший набор. Допустимые подходы (выбор архитектора, можно комбинировать):
|
||||
- TTL на запись кэша (напр. `ORCH_PLANE_STATES_TTL_S`, дефолт разумный, 0/неуст. = прежнее поведение для совместимости);
|
||||
- flush-on-unknown: при детекте неизвестного статуса в вебхуке/реконсилере — вызвать существующий `reload_project_states(pid)` и перезапросить;
|
||||
- админ-эндпоинт/сигнал для ручного flush без рестарта.
|
||||
`reload_project_states()` уже существует — переиспользовать как примитив сброса, новую логику сброса не дублировать.
|
||||
|
||||
## 4. Изменения API
|
||||
- Новых обязательных endpoint'ов нет.
|
||||
- Опционально (TR-4, на усмотрение архитектора): admin-эндпоинт сброса кэша статусов (напр. `POST /admin/plane-states/reload`) — если выбран этот вариант flush. Должен быть защищён/идемпотентен; документировать в README таблице API.
|
||||
- Снимок `GET /queue` (блок `reconcile`) — без ломающих изменений; допустимо добавить поля наблюдаемости (skip-терминалов / dedup-счётчик).
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
**Нет.** Дедуп TR-3 реализуется in-memory (best-effort, как существующие счётчики `unblocked_total`/`last_unblocked`, AC-11 ORCH-053 допускает их сброс при рестарте) либо через сравнение живого состояния Plane/БД. Миграции не требуются.
|
||||
|
||||
## 6. Требования к новым QG checks
|
||||
Нет. Реестр `QG_CHECKS` не меняется.
|
||||
|
||||
## 7. Инварианты (обязаны сохраниться)
|
||||
- INV-1. Source of truth F-2 — Plane (НЕ меняем).
|
||||
- INV-2. never-raise на единицу работы (per-issue / per-project / per-tick) сохранён.
|
||||
- INV-3. Kill-switch `ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2) — работают.
|
||||
- INV-4. F-1 / F-3 поведение не изменено.
|
||||
- INV-5. 0 jobs / 0 токенов для синхронизированных задач (как сейчас) сохранено.
|
||||
- INV-6. Легитимная разблокировка реально-потерянного approved/in_progress webhook продолжает работать (нет регресса F-2).
|
||||
- INV-7. Self-hosting: тик reconciler НИКОГДА не рестартит/не роняет прод-контейнер.
|
||||
|
||||
## 8. Артефакты pipeline, которые надо обновить в ТОМ ЖЕ PR
|
||||
- `CLAUDE.md` — если меняется наблюдаемое поведение reconciler/кэша (раздел про reconciler/правила).
|
||||
- `docs/architecture/README.md` — секция «Reconciler … (ORCH-053)»: уточнить исключение терминалов + дедуп; при TR-4 — секция «Plane Sync».
|
||||
- `docs/architecture/adr/adr-0007-reconciler.md` (или новый per-WI ADR `docs/work-items/ORCH-068/06-adr/ADR-001-…`) — зафиксировать решение по терминалам/группе состояния и по кэшу.
|
||||
- `CHANGELOG.md` — запись о фиксе (`fix:`).
|
||||
- `.env.example` / `.env.staging` — если введён новый флаг (TTL/kill-switch).
|
||||
|
||||
## 9. Замечания по приёмке/тестированию
|
||||
- Регресс-тест ОБЯЗАН покрывать: задача в `Done` (синхронизирована) → тик F-2 = 0 нотификаций, на enduro И orchestrator проектах (terminal не зависит от алиасинга).
|
||||
- Тест НЕ должен делать реальных сетевых вызовов — мокать `list_issues_by_state` / `get_project_states` / `send_telegram` / `_dispatch` (handle_*).
|
||||
84
docs/work-items/ORCH-068/03-acceptance-criteria.md
Normal file
84
docs/work-items/ORCH-068/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Критерии приёмки — ORCH-068
|
||||
|
||||
Формат: каждый AC имеет чёткое условие PASS/FAIL. Задача принимается только при ВСЕХ PASS.
|
||||
|
||||
## Основное (P0) — livelock / спам
|
||||
|
||||
### AC-1. Синхронизированная done-задача → тишина
|
||||
**Дано:** задача в Plane `state=Done`, БД `stage=done`, активных job нет.
|
||||
**Когда:** выполняется один тик F-2 (`reconcile_plane_once` / `_reconcile_plane_project`).
|
||||
- PASS: `_note_unblock` НЕ вызван; `send_telegram` НЕ вызван; `unblocked_total` не изменился; создано 0 jobs.
|
||||
- FAIL: любое уведомление/лог «разблокирована»/инкремент счётчика для этой задачи.
|
||||
|
||||
### AC-2. Терминалы исключены из actionable-выборки
|
||||
**Дано:** проект, где `Done` (и/или `Cancelled`) по UUID совпадает/«схлопнут» с `approved`/`rejected`.
|
||||
**Когда:** `_reconcile_plane_project` формирует набор и обходит issues.
|
||||
- PASS: issue в терминальном статусе (completed-группа / `Cancelled`) НЕ попадает ни в одну из веток `in_progress/approved/rejected`; для неё F-2 — no-op silence.
|
||||
- FAIL: терминальная issue заходит в ветку approved/rejected/in_progress.
|
||||
|
||||
### AC-3. `_note_unblock` только при реальном state change
|
||||
**Дано:** dispatch обработчика (`handle_verdict`/`handle_status_start`) фактически НЕ изменил стадию задачи (no-op, задача уже в целевом состоянии).
|
||||
- PASS: `_note_unblock` НЕ вызван.
|
||||
- FAIL: `_note_unblock` вызван после no-op dispatch.
|
||||
|
||||
### AC-4. Дедуп по неизменному состоянию
|
||||
**Дано:** две последовательные итерации тика по одной и той же синхронизированной задаче, состояние между тиками не менялось.
|
||||
- PASS: суммарно 0 повторных уведомлений по этой задаче.
|
||||
- FAIL: повторное уведомление на втором тике без изменения состояния.
|
||||
|
||||
### AC-5. Нет регресса легитимной разблокировки F-2
|
||||
**Дано:** задача, у которой Plane=`Approved`, а локальная стадия НЕ продвинулась (реально потерянный verdict-webhook), grace выдержан, активных job нет.
|
||||
**Когда:** тик F-2.
|
||||
- PASS: `handle_verdict(approved)` доигран; задача продвинута; `_note_unblock` вызван РОВНО один раз (реальный state change).
|
||||
- FAIL: задача не продвинута ИЛИ нотификация не отправлена ИЛИ отправлена многократно.
|
||||
|
||||
### AC-6. Аналогично для in_progress-старта и rejected-отката
|
||||
- PASS: потерянный `In Progress` (task is None) → старт пайплайна + 1 unblock; потерянный `Rejected` → откат + 1 unblock — оба только при реальном изменении.
|
||||
- FAIL: ложный/повторный unblock или отсутствие легитимного.
|
||||
|
||||
## Инварианты (P0)
|
||||
|
||||
### AC-7. Деньги/ресурсы не тратятся на синхронизированные задачи
|
||||
- PASS: 0 jobs, 0 agent_runs, 0 токенов для done/cancelled задач (как до бага).
|
||||
- FAIL: любой созданный job/agent_run.
|
||||
|
||||
### AC-8. never-raise сохранён
|
||||
**Дано:** `list_issues_by_state` / `get_project_states` / `_dispatch` / `send_telegram` бросают исключение.
|
||||
- PASS: тик не падает; ошибка изолирована (per-issue / per-project), логируется; остальные задачи обрабатываются.
|
||||
- FAIL: непойманное исключение роняет тик/поток.
|
||||
|
||||
### AC-9. Kill-switch'и работают
|
||||
- PASS: `ORCH_RECONCILE_ENABLED=false` → F-1 и F-2 не выполняются; `ORCH_RECONCILE_PLANE_ENABLED=false` → F-2 не выполняется, F-1 работает.
|
||||
- FAIL: любой свитч не гасит соответствующую ветку.
|
||||
|
||||
### AC-10. F-1 / F-3 / реестры / схема БД не изменены
|
||||
- PASS: `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, контракты `handle_*`, поведение F-1/F-3 — без изменений (diff не затрагивает).
|
||||
- FAIL: любое изменение перечисленного.
|
||||
|
||||
### AC-11. Self-hosting безопасность
|
||||
- PASS: ни один путь reconciler не рестартит/не роняет прод-контейнер `orchestrator`.
|
||||
- FAIL: обратное.
|
||||
|
||||
## Secondary (P1) — кэш статусов
|
||||
|
||||
### AC-12. Устаревший `_STATES_CACHE` обновляется без рестарта
|
||||
**Дано:** после старта процесса в Plane появился новый статус (его UUID отсутствует в кэше).
|
||||
**Когда:** срабатывает выбранный механизм (TTL истёк / flush-on-unknown / ручной flush).
|
||||
- PASS: следующий `get_project_states` возвращает свежий набор, включающий новый статус; webhook на новый статус даёт корректное pipeline-действие БЕЗ рестарта.
|
||||
- FAIL: процесс продолжает отдавать устаревший набор → «no pipeline action».
|
||||
|
||||
### AC-13. Совместимость кэша по умолчанию
|
||||
- PASS: при дефолтных настройках (TTL не задан / flush не сработал) поведение `get_project_states` не регрессирует; enduro по-прежнему получает свои UUID, fallback на `_DEFAULT_STATES` при недоступности API сохранён.
|
||||
- FAIL: регресс резолва статусов или потеря fallback.
|
||||
|
||||
## Документация (P0 по правилам проекта)
|
||||
|
||||
### AC-14. Документация обновлена в том же PR
|
||||
- PASS: обновлены применимые из {`docs/architecture/README.md` (Reconciler/Plane Sync), ADR, `CHANGELOG.md`, `CLAUDE.md`, `.env.example`}; reviewer подтверждает.
|
||||
- FAIL: поведение изменено, доки/ADR/CHANGELOG не обновлены.
|
||||
|
||||
## Тесты (P0)
|
||||
|
||||
### AC-15. `pytest tests/ -q` зелёный
|
||||
- PASS: весь набор тестов проходит; добавлены регресс-тесты из `04-test-plan.yaml`, включая done→silence на enduro и orchestrator.
|
||||
- FAIL: любой красный тест или отсутствие регресс-теста на основной баг.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user